Errors and limits
Status codes, error bodies, rate limits and quota. Which to retry.
Every error has the same shape, and the code is stable enough to branch on.
{
"error": {
"code": "quota_exhausted",
"message": "This month's allowance is used up. It renews on your billing date, or you can upgrade now."
}
}
Status codes
| Status | Code | Retry? | |
|---|---|---|---|
| 401 | missing_api_key | no | No key on the request. |
| 401 | invalid_api_key | no | Not recognised. Revoked, or from the other environment. |
| 402 | quota_exhausted | no | Out of credits. Wait for the renewal or upgrade. |
| 422 | nothing_to_check | no | /signup with none of name, email or bio. |
| 422 | ai_unavailable | no | "ai": true on /name, /email or /url. No model reads those, and accepting the flag and ignoring it would be a model call you were priced for and never got. Send false or leave it out. |
| 422 | unknown_rule | no | A name inside rules that is not a category, subject, lead type or term list. The message names it and lists what was expected, because a misspelled line acts on nothing and looks exactly like a rule that works. |
| 400 | invalid_idempotency_key | no | The key is longer than 255 characters. |
| 409 | idempotency_in_flight | yes | Your earlier call with this key is still running. |
| 422 | idempotency_key_reused | no | That key was used for a different body. Details. |
| 422 | - | no | Validation failed. The body lists the fields. |
| 422 | image_too_large | no | An inline picture over the size this endpoint accepts. Send a smaller one, or a URL. |
| 422 | unknown_project | no | You named a project this organization does not have. Nothing was judged. |
| 422 | unknown_policy | no | You named a policy this account does not have. Nothing was judged. |
| 404 | record_not_found | no | No verdict with that id on this account. |
| 404 | batch_not_found | no | No batch with that id on this account. |
| 404 | key_not_found | no | POST /keys/{id}/revoke for a key this account does not have. |
| 429 | too_many_attempts | yes | Too many requests with a key we do not recognise, counted by address. Fix the key. |
| 429 | rate_limited | yes | Too many requests. Back off and retry. |
402 and 429 are not the same thing
429 means come back in a minute. 402 means come back with a bigger plan. A client that retries both will hammer a 402 forever and never succeed.
This is the one distinction worth getting right in your client, because the correct behaviour is opposite in each case. Retry 429 with backoff. On 402, stop calling and alert somebody: no amount of retrying will produce credits.
A 402 body carries your credit state too, so you can log how long the wait is:
{
"error": { "code": "quota_exhausted", "message": "..." },
"credits": {
"remaining": 5,
"required": 10,
"renews_at": "2026-09-30T00:00:00+00:00"
}
}
required is what the call you just made is estimated to cost: the check,
plus what the model would probably use if it reads it. For a
batch it is the whole list added up. It is
why a 402 can arrive while credits remain: the check prices this call, not one
credit. An account with five left cannot start an image call estimated at ten, but the
same account can still moderate text without the model, and the message says so.
Retrying with "ai": false is often the right move.
You can see all of it, at any time and without spending anything, at
GET /api/v1/usage. That
endpoint is outside the quota check, so it answers even at zero.
Rate limits
| Key | Limit |
|---|---|
tf_live_… | 600 requests per minute |
tf_test_… | 60 requests per minute |
| No key | 20 per minute, by IP, and then a 401 anyway |
/batch | 60 per minute (10 on a test key, 5 with no key), in its own bucket |
/ping, /usage, /records, /batches, /keys | 120 per minute, in their own bucket |
Each in its own bucket, so a backfill cannot spend the allowance your live traffic needs, and polling a batch or the review queue costs nothing against either.
Counted per key, not per IP. Every call from one customer arrives from the same handful of addresses, so an IP limit either throttles a whole customer at once or is set so high it stops nobody.
Quota and what a call costs
Credits are checked before the work and charged after it, and only for what actually ran. Before the call, its price is estimated, and a call your credits cannot cover is refused without being made. After it, you are charged what it really used. Charging up front would mean billing for a model reading on every request, including the ones the cheap detectors settled, which is most of them.
| What ran | Credits |
|---|---|
| The instant detectors | 1 |
| The model read it as well | 1, plus the tokens it used, rounded up to the next credit |
| A repeat answered from cache | 1 |
The model's tokens are billed at its rate in credits per million tokens:
gpt-5.6-luna is 2,000 in and
12,000 out. A comment comes to about 8
credits in all, a picture to about 10, and longer text costs more.
used_ai in every successful response tells you whether the model read it,
and credits.charged says what it actually cost. Test keys run the free checks,
never the model, are charged nothing, and are never refused for want of credits.
Content this service has judged before is answered from cache and billed as one check
whatever it originally cost, so repeats are cheap. A call that
failed is never billed at all, and a retry of one that timed out need not be
either, if you send an
Idempotency-Key.