API reference
Errors
Errors use standard HTTP status codes. Every error body carries a human-readable `error` sentence and a stable `code` you can branch on — branch on `code`, never on the wording of `error`, which may be reworded at any time.
{
"error": "Insufficient credits: this call costs 24, balance is 7.8.",
"code": "insufficient_credits",
"need": 24,
"have": 7.8,
"topup_url": "https://you.bot/dashboard/billing"
}On chat models, image input is refused rather than ignored: attaching an image to a chat model whose reference has no `images` parameter comes back 400 with `image_input_not_supported` and is not charged — we do not answer a question about a picture the model never received. On media models (image, video, audio), an `images` key is not part of the schema and is dropped like any other unknown field; send media through the input fields that model documents.
`need` and `have` are credits and appear only on a shortfall; `topup_url` appears only where topping up is the remedy, and it is a signed-in page, so send a person to it rather than a script. A 402 whose code is `spend_cap_daily` or `spend_cap_monthly` is your own key's limit, and `pricing_unavailable` is a fault on our side — neither is fixed by topping up, so do not retry those.
**We tell the model who it is — unless you send your own system instruction.** When you omit `system`, we add one that names the model you called and instructs it to answer with that name if asked, rather than deflecting or naming another. This was measured: without that clause a model answers with a generic description of itself, and sometimes invents a version that does not exist.
What a model says about OTHER versions is still not evidence either way: asked about a version that does not exist, a model will often confirm it exists and even give a release date, then contradict itself later in the same conversation. The record is the `model` field returned on every call — read from the model's own reply — together with your usage log, where each row also shows which API key it arrived on. If either ever shows a model you did not request, send us the task id and we will trace it.
| code | When |
|---|---|
| insufficient_credits | 402 — the account balance will not cover the call. |
| spend_cap_daily / spend_cap_monthly | 402 — this API key's own spend limit was reached. |
| model_not_allowed | 403 — this API key is restricted to a list of models and the requested model is not on it. Nothing ran and nothing was charged. |
| pricing_unavailable | 402 — the model is misconfigured on our side. Nothing ran and nothing was charged. |
| invalid_request | 400 — parameters the model will not accept. |
| unauthorized | 401 — missing or invalid API key. |
| forbidden | 403 — IP not allowed for this key, or the account/model is unavailable. |
| rate_limited | 429 — retry after the Retry-After header. |
| search_not_performed | 422 — `web_search` was requested but the reply came back without an answer. Nothing was charged; retry without `web_search`. |
| duplicate_request | 409 — an identical submit arrived moments ago. Nothing was charged; retry is safe. |
| payload_too_large | 413 — the prompt, an attachment, or the account's upload quota is over the limit. |
| not_found | 404 — the task or file was not found, or it has expired. |
| timeout | 504 — the upstream took too long. Nothing was delivered; retry is safe. |
| upstream_error / unavailable / internal_error | 502 / 503 / 500 — a media task that produced no result is refunded. |
| image_input_not_supported | 400 — this model takes text only. A model that accepts images lists an `images` parameter in its own reference; one that does not, refuses here rather than answering about a picture it never received. |
| too_many_images | 400 — more images than that model's `images` parameter allows in one message, or more than 8 across the whole request. |
| image_too_large | 400 — one attachment is over the per-image size limit. |
| image_format_unsupported | 400 — not JPEG, PNG, GIF or WebP. Read from the file's own bytes, so a data: URL that names the wrong type is refused on its content, not its label. |
| image_unreadable | 400 — the image, or its pixel size, could not be read. Send a data: URL, an https URL we can fetch, or an upload from this account. |
| capability_not_supported | 400 — a capability was requested that the model does not offer (for example `web_search: true` on a model whose API reference lists no `web_search` parameter). Refused with nothing charged, rather than answered as if the capability had run. Web search is available only where the model's own reference lists it. |
| image_not_received | 422 — the model answered that it did not receive the attached image. A supplier route can strip an image block, and a confident answer about a picture the model never saw is worse than an error, so the turn is refused and NOT charged. Retry. |
| empty_reply | 422 — the model returned no answer text (reasoning or a tool call only, or an empty response). Nothing was charged; retry. |
| unknown_image_field | 400 — images were sent under a key the model has no parameter for (`input_urls`, `image_urls`, `imageUrl`, …). Chat models that accept images take them as `images`; the request is refused rather than answered without them, and nothing is charged. |
| image_placement_unsupported | 400 — an image is attached to a message we do not send to the model (a system message, an assistant message, or a turn older than the last 20). Attach images to a user message in the current turn; they are refused rather than silently dropped. |
| prompt_and_messages | 400 — both `prompt` and `messages` were sent. Pick one; we do not choose for you, because the other one would disappear without a word. |
| messages_unsupported | 400 — `messages` was sent to a model that is not a chat model. Media models take a single `prompt`. |
| messages_not_array | 400 — `messages` is not an array of `{ "role", "content" }` objects. A conversation flattened into one string is not a conversation; send real turns. |
| messages_empty | 400 — `messages` is an empty array. Send at least one message, or use `prompt`. |
| too_many_messages | 400 — more than 20 messages in one request. Trim the oldest turns yourself, so you know exactly what the model sees; we do not drop them for you. |
| message_not_object | 400 — one entry of `messages` is not an object. The error names its index. |
| message_role_unknown | 400 — a message `role` is missing or is not `user`, `assistant` or `system`. An unrecognised role is refused rather than treated as the user speaking. |
| message_content_type | 400 — a message `content` is neither a string nor an array of content blocks. |
| message_empty | 400 — a message carries neither text nor images. Leave it out instead; an empty turn would otherwise vanish on the way upstream. |
| Status | Meaning |
|---|---|
| 400 | Invalid or missing parameters for the model. |
| 401 | Missing or invalid API key. |
| 402 | Insufficient credits — top up in Billing. |
| 403 | Request IP not allowed for this key, or the account/model is unavailable. |
| 404 | Task or file not found, or expired. |
| 409 | Duplicate submit inside a short window — nothing was charged. |
| 413 | Payload too large — prompt, attachment or upload quota. |
| 422 | The upstream answered, but the answer was not usable (no text, an image it did not receive, or search results instead of an answer). Nothing was charged; the body names which. A 4xx because the body must reach you — a 5xx body can be replaced by the CDN in front of us. |
| 429 | Rate limited — slow down and retry after the Retry-After header. |
| 504 | Upstream timed out — nothing was delivered; retry is safe. |
| 5xx | Upstream or gateway error — a media task that produced no result is refunded. |