Errors & availability
Which failures are facts, which are transient, and which you should retry. Availability is typed so you never have to guess from a status code.
Availability is a field, not a status code
HTTP status is too coarse to branch on. “This account is private” and “this
account never existed” both arrive as 404 elsewhere, and they call for
completely different handling. So every response carries an availability block:
{ "availability": { "status": "private", "reason": "account_is_private" } }
status |
Meaning | Retry? | Charged |
|---|---|---|---|
available |
You have the data | — | Yes, unless cached |
not_found |
Deleted, or never existed | No | No |
private |
Exists, not publicly visible | No | No |
unsupported |
We do not cover this platform or resource | No | No |
unavailable |
Temporarily unreachable upstream | Yes, backoff | No |
upstream_unavailable |
Our provider is failing | Yes, backoff | No |
Branch on availability.status, not the HTTP code. The status code tells
your HTTP client what to do; this field tells your application what happened.
The retry rule
The first four rows are facts about the world. Retrying a deleted post does not undelete it — it burns time and, on providers that charge for the lookup, money. The last two are transient and worth retrying with exponential backoff.
TERMINAL = {"not_found", "private", "unsupported"}
def handle(resp):
status = resp["availability"]["status"]
if status == "available":
return resp
if status in TERMINAL:
mark_gone(resp) # record it; stop asking
return None
raise Transient(status) # retry with backoff
HTTP status codes
| Code | Meaning | Charged |
|---|---|---|
200 |
Request succeeded — check availability for what you got |
Depends |
400 |
Malformed request, unparseable URL | No |
401 |
Missing, malformed or revoked key | No |
402 |
Credits exhausted and overage disabled | No |
409 |
Idempotency-Key reused with a different request |
No |
429 |
Rate limited — see Retry-After |
No |
5xx |
Our fault | No |
A 200 with availability.status of not_found is not a contradiction: the
request worked, and the answer is that the resource is gone. That answer is
free.
Cancelled and timed-out requests
If your client gives up on a request — an AbortController, a timeout shorter
than ours, a dropped connection, a killed process — the response never reaches
you, but the work may already have been done on our side.
A cancelled request can still be billed. If we had already fetched from the source when you disconnected, that fetch is charged: we paid the provider, and your socket closing does not refund us. If you cancelled before we called upstream, nothing is charged.
The data is not lost. The result is still written to cache, so retrying the same resource is a cache hit and costs 0 credits. Cancelling and retrying costs you one fetch, not two.
Use an Idempotency-Key if you retry. With the same key, the retry returns
the original stored response with no second charge at all — and it removes the
one genuinely awkward part of a cancellation, which is that you cannot tell from
the client whether you were billed:
key = str(uuid.uuid4())
for attempt in range(3):
try:
r = client.get("/v1/profile", params={"url": url},
headers={"Idempotency-Key": key}, timeout=10)
break
except (httpx.TimeoutException, httpx.ReadError):
continue # same key — the retry cannot double-charge
Do not generate a new key per attempt. A fresh key on each retry is a fresh request, and each one that reaches upstream is billed.
Error body
{
"error": {
"code": "unparseable_url",
"message": "That URL is not a supported TikTok, Instagram or YouTube resource.",
"request_id": "req_9c2a0e4482118893"
}
}
Always log request_id. It is on every response, success or failure, and it is
the fastest way for us to find what happened.
Rate limiting
429 carries Retry-After in seconds. Respect it — retrying sooner will not
succeed and counts against your next window. Rate limits are per project and
scale with your plan; see authentication.
Last updated 27 August 2026