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 | Not needed | Yes |
not_found |
Deleted, or never existed | No | Yes |
empty |
The account or post exists and has nothing to return | No | Yes |
private |
Exists, not publicly visible | No | Yes |
unsupported |
We do not cover this platform or resource | No | No |
unavailable |
Data temporarily unreachable | Yes, backoff | No |
upstream_unavailable |
Temporary data retrieval failure | 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 five rows are facts about the world. Retrying a deleted post does
not undelete it. It burns time and, because a not_found, empty or
private is charged like any lookup, credits. The last two are transient; retry them with exponential backoff.
On the Facebook APIs a missing, deleted, private or region-blocked post or
profile all look the same: Facebook shows one “This content isn’t available”
page. Once a second check confirms it, it arrives as not_found (charged),
never private. A read we could not confirm is upstream_unavailable (503, not
charged), so retry it.
empty is the one to read carefully. It means the container is real and holds
nothing: an account with no posts yet, a video with comments turned off, the
last page of a cursor walk. It is deliberately not not_found, because the
account is not missing. Reporting one as the other would send you to debug a
profile that is fine. It is charged like any lookup, and paginating past it
returns the same answer, so stop.
TERMINAL = {"not_found", "empty", "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 | 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. A live lookup
that returns this answer is charged at the operation’s normal price.
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 a billable lookup was already completed when you disconnected, its credit charge still applies. Closing the connection does not undo the processing work. If you cancelled before the fetch started, nothing is charged.
The data is not lost. The result is still written, so an immediate retry of the same resource returns it without a second fetch.
Use an Idempotency-Key if you retry. With the same key, the retry returns
the original stored response with no second charge at all. It also settles the
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/tiktok/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 billable live lookup is charged separately.
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 4 October 2026