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