Errors
Status codes, the error body, and how to handle limits.
Error body
Every error is JSON with a detail message and a code:
{ "detail": "This capability is still being set up for your account.", "code": "tenant_provisioning" }Read detail to show what went wrong, and match on code in your code. The code is either one of the gateway's (insufficient_credits, concurrency_limit_exceeded, upstream_error and others below) or the family's own, as the family spells it (KNOWLEDGE_BASE_NOT_FOUND, NOT_FOUND). A family error that comes without a code gets the gateway's code for its status, such as not_found for a 404.
An error from a family's service keeps the fields the service sent and gains detail and code beside them. For example:
{
"detail": "knowledgeBaseId 'kb_1' does not name a knowledge base in this workspace",
"code": "KNOWLEDGE_BASE_NOT_FOUND",
"status": "error",
"message": "knowledgeBaseId 'kb_1' does not name a knowledge base in this workspace",
"errors": [
{ "code": "KNOWLEDGE_BASE_NOT_FOUND", "field": null, "message": "knowledgeBaseId 'kb_1' does not name a knowledge base in this workspace" }
]
}A request that fails validation has one sentence in detail, "code": "validation_error", and one item per problem in errors:
{
"detail": "page: Input should be greater than or equal to 1",
"code": "validation_error",
"errors": [{ "code": "greater_than_equal", "field": "page", "message": "Input should be greater than or equal to 1" }]
}A family that validates the request itself keeps its own spelling and fields: "code": "VALIDATION_ERROR" with message and status beside detail and errors. AEO answers 400 for a bad request where other families answer 422.
If a service answers with something that isn't JSON, you get {"detail": "...", "code": "upstream_error"} instead. A successful answer is always passed back exactly as the service sent it.
Rarely, while our servers are restarting, an error comes from our network edge instead of the API: a 502 or 504 as an HTML page or a Cloudflare JSON error, without an X-Request-ID header. The API itself never answers either status. Treat it like a 503: retry with a growing delay.
Status codes
| Status | Meaning |
|---|---|
400 | The request can't be sent as it is. vertical_required: the operation needs the X-Catalog-Vertical header, and the detail lists the catalogs it takes. vertical_unknown: the header names a catalog the operation doesn't serve; the detail lists the ones it does. Also query_parameter_missing, query_parameter_invalid and bad_request. |
401 | api_key_missing: no key was sent; send it in the X-API-KEY header. api_key_invalid: the key is unknown, revoked or expired; check it in the console, or create a new one. |
402 | Your credits don't cover the call, or the call costs more than your plan allows for one call. See Credits. |
403 | The key can't make this request. The detail says why: the key doesn't include the family, your plan no longer does, the account is disabled, your credits are blocked, the key is limited to other addresses or websites, or a publishable key tried to write. |
404 | Not an operation this family offers (not_found), or no such family (family_not_found). |
405 | The path exists but not for this method (method_not_allowed). The Allow header lists the methods it takes. HEAD works wherever GET does. |
413 | The request body is too large (over 25 MB), or, on an operation priced by the size of its body or one of its fields, too large to price. |
415 | The body isn't the kind the operation takes (unsupported_media_type): JSON sent to an operation that takes form data, or form data to one that takes JSON. The detail names the content type to send. |
422 | A parameter the gateway checks itself isn't valid (validation_error). See Error body. |
429 | A limit is used up. See below. |
500 | The gateway itself failed on the call (internal_error). It is never charged. Try again in a moment; if it keeps happening, quote the X-Request-ID header to support. |
503 | Nothing is charged. Either the family's service failed on the call: upstream_unreachable (it could not be reached), upstream_timeout (it took too long to answer), upstream_sign_in_failed (it refused the gateway), upstream_unreadable (its answer could not be read), or it answered 502 or 504 itself, passed on with its own code (or service_unavailable) and an X-Labs-Error: provider_bad_gateway or provider_gateway_timeout header; retry these with a growing delay. Or the call is not made: tenant_provisioning: the family is still being set up for your account, and tenant_renewing: it is being renewed; both say "retryable": true, so retry after Retry-After. The Families page of the console shows the progress. tenant_setup_stopped: setting it up stopped after repeated failures ("retryable": false); we have been told, so try again later or contact support. tenant_not_set_up: contact support. capability_unavailable: the operation isn't available yet. upstream_edge_block: the service's network edge refused the call; retry once after Retry-After. credits_unavailable: your credits can't be checked right now; retry after Retry-After: 5. |
Every answer from a family's service carries an X-Request-ID header. Include it when you contact support.
Limits
Your plan sets limits on how fast you can call. They are shared by all the keys of an application's owner:
- Requests per minute. Past it you get
429withRetry-After(seconds to wait) andX-RateLimit-Limit. A refused request doesn't count, so waiting as long asRetry-Aftersays gets you in. - Calls at once, if your plan caps them. Counted across all your keys and families. One more call while the cap is full gets
429with"code": "concurrency_limit_exceeded",Retry-After: 1andX-Concurrency-Limit. The call is not made and costs nothing; try again once one of your calls has answered.
{ "detail": "Your plan allows 5 calls at once. Wait for one to finish before starting another.", "code": "concurrency_limit_exceeded" }Credits
There is no request quota. Every call to a family's operation is paid in credits from your wallet, which all the applications of an owner share. Your plan grants an amount of credits each billing period, and you can buy more as top-ups on the Billing page. Each operation has its own price, shown for each endpoint on the Families page.
A charged answer carries X-Credits-Charged (what the call cost) and X-Credits-Remaining (what is left). An operation that starts a job and answers 202 carries X-Credits-Pending instead: the amount held while the job runs. The charge is settled from the job's result once it finishes and can differ from the hold; the settled amount shows in the credit history on the Billing page, not in a header. These calls cost nothing:
GET /api-keys/verify- requests that are refused, such as a
401,403,402or429 - requests that fail: any
5xx, and any4xxsuch as wrong parameters or an unknown item. The exception is a4xxfrom a service that did work before refusing; then the call is still charged.
A refused or failed call carries X-Credits-Charged: 0: nothing was taken.
If you run out, the call is not made and you get 402:
{ "detail": "Not enough credits for this request. Top up or upgrade your plan.", "code": "insufficient_credits", "needed": 5, "available": 2 }The body also has top_up_url (where to buy credits) and, if you are on a free plan, upgrade_url, when they are set up. The answer carries X-Credits-Needed and X-Credits-Available.
If one call costs more than your plan allows for a single call (max_credits_per_call), you get 402 with "code": "call_over_plan_limit", needed, max_credits_per_call and upgrade_url, even if you have credits left. Only a bigger plan helps.
If your credits are blocked, for instance after a chargeback, calls get 403 with "detail": "Your credits are blocked. Contact support.".
Retrying
Retry a 429 after Retry-After seconds. Retry a 503 with a growing delay, and a 502 or 504 from our network edge the same way. Don't retry a 402 in a loop: it lasts until you have more credits, or a bigger plan.
import time
import requests
def call_with_retry(url, api_key, attempts=5):
for _ in range(attempts):
response = requests.get(url, headers={"X-API-KEY": api_key})
if response.status_code != 429:
return response
time.sleep(int(response.headers.get("Retry-After", "1")))
return response
