Errors
Every error is JSON with an error string and, where it helps, a hint:
{ "error": "too many urls", "hint": "max 500 per request, got 501" }
HTTP statuses
| status | when |
|---|---|
| 400 | bad input — missing url, unparseable body, too many items, unrecognized platform, days out of range |
| 401 | missing, unknown or disabled key; admin endpoint without the admin token |
| 403 | scoped key reaching outside its scopes (/v1/raw only) |
| 404 | the post or profile does not exist, is deleted, or is private — never a zeroed result |
| 405 | wrong method (e.g. GET on /v1/posts) |
| 502 | upstream unreachable, or upstream returned an error we could not turn into a 404 |
404 is a feature
The upstream provider answers HTTP 200 for a deleted post and simply omits the post object. Read naively, a removed video looks like a genuine crash to 0 views — which, if the numbers feed view tracking, is a payout dispute waiting to happen.
TikData checks for the object explicitly and answers 404 instead. The same applies to profiles: X returns 200 with status: "notfound" and Instagram returns 200 with status: false for accounts that do not exist, and both become 404 here.
Missing individual stats still come back as 0 inside an otherwise-successful response — platforms omit different fields, and a partial answer beats a failed refresh.
Private vs deleted
Not distinguishable through the upstream provider. Verified 2026-09-08 against a known-private TikTok video across seven endpoints: every one returns a byte-identical body for private and for a bad id. The signal exists at TikTok itself (statusMsg: "status_self_see" vs "item doesn't exist") but is stripped before it reaches us. See videos/bulk_stats for the per-item error codes this leaves us with.
Per-item errors in batch endpoints
POST /v1/posts and POST /v1/videos/bulk_stats never fail the whole request for one bad item. Each result carries ok: true|false, and failed items carry an error code:
| code | meaning |
|---|---|
not_found |
well-formed reference, but the platform returned no post (deleted, private, or never existed) |
invalid_id |
the id is malformed — TikTok ids encode a creation timestamp, so this is decided offline with no upstream call |
unsupported_platform |
platform was given but is not tiktok, instagram, youtube or x |
bad_input |
neither a usable id nor url, or no platform could be determined |
upstream_error |
the provider returned a non-2xx we could not classify |
upstream_unreachable |
timeout or network failure reaching the provider |
Upstream flakiness, and how TikData handles it
The provider fails in two different ways, and they need opposite responses.
The node wobbles. 5xx, dropped connections and the provider's own 400 "Request failed. Please retry." arrive in bursts that hit every endpoint and every platform at once — a run of ~100 calls pushed the whole account into 502s for about 40 seconds on 2026-09-12, TikTok, X, Instagram and YouTube alike. Switching endpoints cannot help with that; only waiting can. So these retry the same endpoint with backoff (2 retries, 400 ms growing).
One backend goes blind. A 200 arrives with the post object simply missing, while a sibling endpoint on a different backend still has it. Retrying the same path is pointless. So these move to the next endpoint in the chain.
The chains
Every entry was verified live against a known-good subject on 2026-09-12.
| operation | TikTok | YouTube | X | |
|---|---|---|---|---|
| post | 3 backends | 2 (v3 at $0.008 last) |
2 | 1 — retry only |
| profile | 1 — retry only | 3 | 1 | 1 — retry only |
| feed | 3 | 3 | 1 | 2 |
Where the table says "1", no working alternate exists on this node: the siblings are 404, or answer 200 without the field that matters (X's fetch_user_about returns no follower count, so it cannot stand in for fetch_user_profile).
What a failed lookup costs
A blind backend is indistinguishable from a genuinely deleted post — both answer 200-with-nothing. Walking the whole chain is what separates them: if every backend says nothing, it really is gone. So a deleted TikTok post costs 3 calls rather than 1. That is the price of never reporting a live post as deleted, and malformed ids are still rejected offline for free as invalid_id.
Feed walks pick a backend on page one and stay on it, because paging cursors are endpoint-specific.
Seeing which backend answered
/v1/post and /v1/profile return two headers:
| header | meaning |
|---|---|
x-router-source |
the upstream path that produced the data |
x-router-attempts |
total upstream requests made, including retries and fallbacks |
/v1/creator/stats reports the same thing in its source field.