API errors and rate limits
The full /api/v1 error code table, rate-limit headers, and how to back off correctly.
Every failure from /api/v1 is the same JSON shape with a stable machine-readable code. Branch on code, not on the message text or the HTTP status alone — messages get reworded, codes never change meaning.
{
"error": {
"code": "not_entitled",
"message": "This isn't part of your purchases. Buy the pack or lifetime access to download it.",
"request_id": "5f3c9a10-2d6b-4b2e-9f77-1c0a8de41b52"
}
}request_id is always populated, and always equals the x-request-id header on the same response — so you can log it from the headers of successful calls too. If you send your own x-request-id (8–64 characters, letters, digits, _ and -), we use yours instead of generating one. Log it either way: quoting it in a support email points us straight at the call that failed.
Error codes
| Code | HTTP | What it means | What to do |
|---|---|---|---|
missing_api_key | 401 | No key on the request. | Send Authorization: Bearer tja_live_… or X-API-Key. |
invalid_api_key | 401 | The key is malformed, unknown, or the wrong kind of credential. | Check it was copied in full. If it starts with TJA_, you sent a license key — see API authentication and keys. |
revoked_api_key | 401 | The key was revoked. | Create a new one in Connections. |
expired_api_key | 401 | The key passed its expiry date. | Create a new one; consider "never expires" for keys you control. |
api_disabled | 403 | API access is not available for this account right now. It is also what the public /v1/catalog/* routes answer while the API is switched off. | Check the Connections tab, which explains the current state. Your keys are untouched. |
not_eligible | 403 | The account has no purchase. | Buy any pack or lifetime access. |
blocked | 403 | The request was refused before anything else was checked; the message is just Access denied. | Nothing to fix in your code. If you believe it is a mistake, contact support with the request_id. |
insufficient_scope | 403 | The key lacks the scope this endpoint needs. | Create a key with downloads:read (or catalog:read) enabled. |
not_entitled | 403 | The item is real but not part of your purchases. | Buy the pack or lifetime access. |
not_found | 404 | No such published pack or asset. | Check the slug. Coming-soon and archived items are 404s on purpose. |
file_unavailable | 404 | The item exists but the requested file is not there — often an edition=authoring a pack does not have. | Retry shortly, or drop the edition parameter. |
range_not_satisfiable | 416 | The Range you asked for starts past the end of the file. | Read Content-Range: bytes */<size> and correct your offset. |
rate_limited | 429 | Too many requests. | Wait for Retry-After, then retry. |
internal_error | 500 | Something broke on our side. | Retry with backoff. If it persists, send us the request_id. |
invalid_api_key deliberately does not distinguish "malformed" from "no such key", api_disabled does not say which switch produced it, and blocked says nothing at all. That is a security choice, not an omission.
Rate limits
Limits are per key for authenticated calls, so one busy script of yours cannot starve another — and a colleague behind the same office IP never pays for your loop. The public catalog is limited per IP address instead, since there is no key to attribute it to.
| Traffic | Default | Window | Keyed by |
|---|---|---|---|
Metadata reads (/v1/me, /v1/packs, /v1/assets, /v1/orders) | 120 requests | 1 minute | your API key |
Downloads (both /download endpoints) | 30 requests | 1 minute | your API key |
Public catalog (/v1/catalog/*) | 120 requests | 1 minute | IP address |
| Failed authentication attempts | 20 attempts | 1 minute | IP address |
Reads and downloads have separate budgets, so listing your library never eats into your download allowance. Downloads are the tight one — each streams a multi-megabyte file — so pull sequentially rather than in parallel.
Rate-limit headers
Every response that reached the rate limiter carries them — successes, 404s, 403 not_entitled, and the 429 itself — so a well-behaved client can pace itself long before it sees a 429. The exceptions are the refusals decided before the limiter runs: blocked, a 500 internal_error, and (on the authenticated routes) api_disabled and not_eligible come back without them. Both spellings go out; use whichever your HTTP client already parses.
RateLimit-Policy: "download";q=30;w=60
RateLimit: "download";r=27;t=41
X-RateLimit-Limit: 30
X-RateLimit-Remaining: 27
X-RateLimit-Reset: 41| Header | Meaning |
|---|---|
RateLimit-Policy | The policy name, its quota (q) and window in seconds (w). |
RateLimit | The policy name, requests remaining (r) and seconds until reset (t). |
X-RateLimit-Limit | Same as q. |
X-RateLimit-Remaining | Same as r. |
X-RateLimit-Reset | Same as t — seconds from now, not a timestamp. |
Policy names you will see: read, download, public, auth.
A 429 additionally carries Retry-After, in seconds:
HTTP/1.1 429 Too Many Requests
Retry-After: 41Backing off politely
fetch() {
for attempt in 1 2 3 4 5; do
code=$(curl -s -o body.json -w '%{http_code}' \
-D headers.txt -H "Authorization: Bearer $TJA_API_KEY" "$1")
[ "$code" = "429" ] || { cat body.json; return 0; }
wait=$(awk 'tolower($1) == "retry-after:" { print $2 }' headers.txt | tr -d '\r')
sleep "${wait:-5}"
done
echo "gave up on $1" >&2
return 1
}Two rules that keep you comfortably inside the limits:
- Honour
Retry-After. It is exact; guessing is not. - Watch
X-RateLimit-Remainingand slow down before it reaches zero, rather than sprinting into a 429 and recovering.
Repeated failed authentication is limited separately and by IP. If you are looping on invalid_api_key, fix the key rather than retrying — you will hit the failed-auth limit and then get 429s that look unrelated.
Troubleshooting
- 401 on every call — check the header name and that the key was copied whole, including the trailing checksum. Try
/v1/mefirst; it is the cheapest test. - 403
insufficient_scopeon downloads only — the key was created withoutdownloads:read. Scopes cannot be edited; create a new key. - 404 on a pack you own — coming-soon packs are not downloadable yet, and archived ones are gone. Confirm the slug against
/v1/packs. - A download that redirects to a sign-in page — you fetched
preview_url(the public preview GLB) instead ofdownload_url, without a key. See API endpoint reference. amountisnullin/v1/orders— an older order never recorded a total. That means unknown, not free.