Concepts
Errors
The problem document every failure returns, the stable codes to branch on, and what to do about each one.
Every failure answers the same way: an HTTP status, a body of type application/problem+json, and inside it a stable code that is the only part of the error you should ever write code against. Statuses tell you the shape of the problem; the code tells you which problem it is.
The shape
Four fields are always there. type is a URL identifying the kind of problem, title is a short human summary, status repeats the HTTP status inside the body, and code is the machine-readable name. Most problems add detail, a sentence about this occurrence, and some add fields of their own — a balance, a limit, the id of a conflicting Run.
402 · `insufficient_balance` — the Balance cannot cover the quoted price (the worst case for Research and enriched search). No Run is created and nothing is charged. A multi-item Run whose Balance runs short later charges its completed items up to the Balance and ends `partial`.
{
"type": "https://docs.shortsintel.com/errors/insufficient_balance",
"title": "Insufficient balance",
"status": 402,
"code": "insufficient_balance",
"detail": "Your balance does not cover this operation. Top up in the console.",
"balance_usd": 0.001,
"required_usd": 0.005
}Branch on the code, never on the prose
code is a contract. Codes are added deliberately, are never renamed, and mean the same thing on every endpoint that emits them. title and detail are prose written for a human reading a log: they get rewritten whenever a clearer wording turns up, and matching on them will break without warning.
The same goes for statuses on their own. Two different codes share 429 and mean different things, and 403 covers two distinct authorization failures. Switch on the code and use the status only to decide whether a retry is plausible at all.
Every code
Credentials
| Code | HTTP | What to do |
|---|---|---|
| missing_authorization | 401 | No bearer credential reached us. Send the Authorization header. |
| invalid_key | 401 | The key is not one of ours. Check you copied all of it. |
| key_revoked | 401 | The key was revoked in the console. Issue a new one. |
| invalid_token | 401 | An access token failed verification — bad, expired, or minted for something else. Refresh it and retry once. |
| organization_not_selected | 403 | The connection was authorized without choosing an organization, so there is no balance to spend. Re-authorize. |
| organization_access_revoked | 403 | The user has left the organization the token acts for. Re-authorize against one they are still in. |
| connection_revoked | 401 | The token verifies, but the connection it was issued under was revoked — consent withdrawn or the client disabled. Re-authorize; retrying will not help. |
The request itself
| Code | HTTP | What to do |
|---|---|---|
| invalid_request | 400 | The request does not match the operation. Read `detail`; retrying unchanged will fail identically. |
| unsupported_platform | 400 | That platform is not one we cover for this operation. See the coverage guide. |
| unsupported_kind | 422 | A valid request for something not served yet, such as a Tracker on a hashtag. Nothing was created or charged; retrying unchanged will fail identically. |
| not_found | 404 | No such thing, including anything belonging to another customer. Never a sign that it exists elsewhere. |
| idempotency_conflict | 409 | This Idempotency-Key was already used with a different payload. Use a new key, or resend the original payload. |
| run_not_ready | 409 | The Run this read draws on has nothing to serve yet. Still running: poll it with `GET /v1/runs/{id}` until it settles as `completed` or `partial`, then read again. Failed: there is nothing to read and polling will not change that; start a new Run. |
Money
| Code | HTTP | What to do |
|---|---|---|
| insufficient_balance | 402 | Top the balance up. On the call itself this means nothing was charged and no work started, so it can be repeated as-is. On one item of a partial Run, the balance ran short mid-Run: the earlier items were charged, this one was not. |
Limits
| Code | HTTP | What to do |
|---|---|---|
| rate_limit_exceeded | 429 | Too many requests on this credential. Wait the seconds in `Retry-After` and retry. |
| concurrency_limit_exceeded | 429 | Too many Runs are already in flight. Wait for one to finish — `Retry-After` says how long to sleep. |
| tracker_limit_exceeded | 409 | You are at the tracker cap. Pause or delete one; retrying will never clear this on its own. |
| webhook_limit_exceeded | 409 | You are at the endpoint cap. Delete an endpoint you no longer deliver to. |
| webhook_rotation_in_progress | 409 | The previous rotation's overlap window is still open. Wait until `overlap_expires_at` before rotating again. |
Upstream and ours
| Code | HTTP | What to do |
|---|---|---|
| media_unavailable | 502 | We could not fetch the media — deleted, private, or refused by the platform. A fact about the video, not a transient failure. |
| vendor_error | 502 | An upstream source failed. Safe to retry with a backoff; the Run was not charged. |
| internal_error | 500 | Something broke on our side. Retry with a backoff, and tell us if it persists. |
The three worth handling first
402 — insufficient balance
Checked before any work starts, so a 402 means nothing happened: no Run, no charge, no partial state to reconcile. The body carries what you have and what the call needed, which is enough to top up and repeat the identical request. For Research and enriched searches the check is against the worst-case quote. The same code can also appear on one item of a partial Run: the balance ran short while the Run was working, so the completed items were charged in order up to the balance and the rest were not. See pricing and estimates for how to avoid meeting it.
409 — idempotency conflict
Replaying an Idempotency-Key with the same payload returns the original Run, which is what makes a retry safe. Replaying it with a different payload is a conflict: we will not quietly do new work under an old key, and we will not quietly return the old answer for a new question. The body names the Run the key already belongs to.
409 · `idempotency_conflict` — the same Idempotency-Key with a different payload.
{
"type": "https://docs.shortsintel.com/errors/idempotency_conflict",
"title": "Idempotency conflict",
"status": 409,
"code": "idempotency_conflict",
"detail": "This Idempotency-Key was used with a different payload.",
"run_id": "run_8f2c1ad04b"
}Generate one key per logical request and reuse it only for retries of that exact request.
429 — too fast, or too much at once
Two codes answer 429, and the difference decides what you should do. rate_limit_exceeded means requests are arriving too quickly on this credential; concurrency_limit_exceeded means too much paid work is already in flight and a slot frees when one of your Runs finishes.
429 · `rate_limit_exceeded` or `concurrency_limit_exceeded`, with `Retry-After`.
{
"type": "https://docs.shortsintel.com/errors/rate_limit_exceeded",
"title": "Rate limit exceeded",
"status": 429,
"code": "rate_limit_exceeded",
"detail": "60 requests per minute per key. Retry after the number of seconds in `Retry-After`.",
"limit": 60
}Both carry Retry-After in the response headers. Sleep for that many seconds — do not invent your own backoff, and do not retry immediately; the limits are described in authentication and limits.
Errors that live on a Run
Not every failure is an HTTP failure. A call that starts work successfully and then cannot finish it answers 200 or 202, and the failure lands on the Run instead: a failed Run carries a problem document in exactly this shape, and a partial Run carries one per item that did not arrive. Same fields, same codes, same rule about branching.