Error codes
Every error from /v1 comes back in one shape, the error envelope, whichever check refused the request. This page describes that shape and lists every code POST /v1/chat/completions and GET /v1/models can return today, with its HTTP status, the situation that causes it, and whether a retry can help. For advice on handling errors inside an application, see Errors.
The error envelope
{
"error": {
"type": "insufficient_quota",
"code": "quota_exhausted",
"message": "Your monthly plan token allowance is used up. ...",
"param": null,
"request_id": "req_6f1c2a9e0b7d4c3a8e5f1b2d",
"limit": "monthly",
"resets_at": "2026-11-01T00:00:00.000Z",
"upgrade_url": "https://syntic.ai/pricing",
"add_credit_url": "https://syntic.ai/agents/billing#api-topup"
}
}The shape is OpenAI-compatible, so client libraries that read error.type, error.code and error.message work unchanged.
| Field | Always present | Description |
|---|---|---|
error.type | Yes | The broad category, fixed by the HTTP status (table below). Decide retry or no retry from this. |
error.code | Yes | A stable snake_case code. Branch detailed handling on this. Handle a code you do not recognise by its type. |
error.message | Yes | A sentence for people. The wording may change; do not parse it. |
error.param | Yes | Always null today. |
error.request_id | Yes | Our id for the request, req_ followed by letters and digits. The same value is in the x-request-id and x-syntic-request-id headers. Quote it to support. See Request IDs. |
error.retry_after | On 429 and 503 | Seconds to wait, equal to the Retry-After header. |
| Extra fields | Some codes | Listed per code below, for example limit, resets_at, max_bytes or upgrade_url. Links in these fields always point at syntic.ai. |
Error responses carry Cache-Control: no-store. Messages never contain text from the model service: no vendor names, hosts, internal model ids or raw error bodies. When the original message cannot be shown, the code’s standard sentence is used instead.
Older deployments answered with a flat body such as {"error": "quota_exhausted", "message": "..."}. If you have to support both, read error.code when error is an object, and error itself when it is a string.
Types
type | HTTP status | Meaning | Retry? |
|---|---|---|---|
invalid_request_error | 400, 405, 413, 422 | The request is malformed, too large, on the wrong path or method, or asks for something not allowed. | No. Fix the request. |
authentication_error | 401 | The credential is missing, unknown, revoked or expired. | No. Fix the credential or sign in again. |
insufficient_quota | 402 | The plan allowance and usage credits cannot pay for the request. | After resets_at, or after adding credits. |
permission_error | 403 | The credential is valid but may not make this request. | No. |
not_found_error | 404 | The model does not exist or is not visible to this credential. | No. |
timeout_error | 408, 504 | The answer took too long. | Yes, with back-off. |
conflict_error | 409 | The request clashes with current state. | Yes, shortly. |
rate_limit_error | 429 | Too many requests, or the model is busy. | Yes, after Retry-After. |
api_error | 500, 502 | A failure on Syntic’s side or in the model service. | Yes, with back-off. |
service_unavailable_error | 503 | A check or dependency is briefly unavailable. | Yes, after Retry-After. |
Any other status from inside the platform is folded into one of these before it reaches you: an unlisted 4xx becomes 400, and an unlisted 5xx becomes 502 (503 for an overloaded service).
Status codes at a glance
This table is generated from the public error catalog in the platform’s error envelope (supabase/functions/api-proxy/error-envelope.ts on develop). type is fixed by the HTTP status; code is the machine-readable reason. stream_error arrives inside a stream that already answered 200. model_not_found and model_not_in_plan are in the catalog on develop but not returned in production yet.
Not in the public catalog (provisional). The platform also returns model_unavailable (400), zero_retention_unavailable (403), feature_unavailable (403) and model_retention_unverifiable (503) today, and Syntic Desktop sign-ins can see org_required, quota_exceeded, prompt_too_large, concurrent_request and temporarily_unavailable. These keep the envelope and the type for their status, but are not catalog codes yet and may be renamed or folded into one. Handle them by type.
400 Bad Request
code | When it happens | Extra fields | Retry? |
|---|---|---|---|
invalid_json | The body could not be parsed as JSON. | none | No. |
messages_required | messages is missing, not an array, or empty. | none | No. |
invalid_request | model is present but not a non-empty string; include_unavailable on GET /v1/models is not true or false; or the model refused the request as malformed (for example, broken tool messages). | none | No. Fix the request. |
context_length_exceeded | The prompt plus max_tokens does not fit in the model’s context window. | none | No. Shorten the conversation or lower max_tokens. |
model_unavailable | Provisional, not in the public catalog. Your organization uses zero data retention and the chosen model needs standard retention. | none | No. Choose another model. |
invalid_surface | GET /v1/models?surface= is not one of api, cli, terminal, desktop. | none | No. |
invalid_capability | GET /v1/models?capability= names an unknown capability. | none | No. |
org_required | Desktop. The sign-in is not bound to a workspace. | none | No. Sign in to a workspace. |
401 Unauthorized
code | When it happens | Retry? |
|---|---|---|
unauthenticated | No Authorization: Bearer ... header was sent. | No. |
invalid_api_key | The key is unknown or inactive. | No. |
revoked_api_key | The key was revoked. | No. Create a new key. |
expired_api_key | The key is past its expiry date. | No. Create a new key. |
invalid_token | A sign-in token failed verification or has expired. | Refresh the token once, then sign in again. See Sign in with Syntic. |
token_revoked | The sign-in session behind the token was revoked, for example by Revoke a sign-in. | No. Sign in again. |
402 Payment Required
quota_exhausted means the request cannot be paid for. The limit field says which cap stopped it:
limit | Meaning | How it clears |
|---|---|---|
monthly | The plan’s monthly token allowance is used up. | Monthly reset, a plan upgrade, or turning on usage credits. |
weekly | The plan’s weekly window is used up. | Weekly reset, or usage credits. |
session | The plan’s rolling 5-hour window is used up. | When the window moves on, or usage credits. |
spend_limit | The organization’s monthly usage-credit spend limit (or a member’s own limit) was reached. | An admin raises the limit, or the next month starts. |
no_credits | The plan allowance is used up and the credit balance cannot cover the request. | Buy credits. |
Extra fields: limit, resets_at (ISO 8601 time, or null), upgrade_url, add_credit_url. When usage cannot be checked at all, the answer is 503 usage_check_unavailable instead (see 5xx). See Handle usage limits.
quota_exceeded (Desktop) means the Free plan’s monthly tokens are used up. Extra field: action_url, the upgrade page.
403 Forbidden
code | When it happens | Extra fields | Retry? |
|---|---|---|---|
public_key_rejected | A pk_live_ publishable key was used. Only secret keys may call /v1. | none | No. |
origin_not_allowed | The key has an origin allowlist and the request’s origin is missing or not on it. | none | No. |
insufficient_scope | The credential lacks the user:inference scope. | required | No. |
account_disabled | The account that owns the credential is disabled. | none | No. |
org_unavailable | The credential’s organization is not available. | none | No. |
org_membership_required | The credential’s owner is no longer an active member of its organization. | none | No. |
plan_required | The organization is on the Free plan (reason: "free_plan") or its billing is inactive (reason: "billing_inactive"). API access needs a paid plan. On Desktop, also a model or feature above the current plan. | reason, upgrade_url; Desktop: action_url, required_plan_id | No. Upgrade or fix billing. |
model_not_in_plan | The named model exists but your plan does not include it. Not live yet — on develop; today chat completions answers with the default model and an x-syntic-model-downgraded header. | upgrade_url | No. Choose a model from GET /v1/models. |
zero_retention_unavailable | Provisional, not in the public catalog. Your organization requires zero data retention and no zero-retention model service is available. | none | No. Contact support. |
feature_unavailable | Provisional, not in the public catalog. Chat has been switched off temporarily across the platform. | feature | Yes, later. |
More on credential errors is in Authentication.
404 Not Found
model_not_found: model is not a model id that GET /v1/models returns for this credential, or GET /v1/models/{id} names a model that is unknown or that this credential cannot use. Names are exact and case-sensitive. Internal engine ids are treated as unknown names. Do not retry; list the models first.
Not live yet — on develop. Production does not return model_not_found yet: today POST /v1/chat/completions answers an unknown name with the default model instead.
405 Method Not Allowed
code | When it happens |
|---|---|
method_not_allowed | /v1/chat/completions was called with a method other than POST, or /v1/models with one other than GET or HEAD. The Allow header lists the accepted methods. |
endpoint_not_supported | A POST reached a /v1 path other than /v1/chat/completions. Nothing was run or billed. |
Do not retry; fix the method or the path.
413 Payload Too Large
code | When it happens | Extra fields |
|---|---|---|
payload_too_large | The body is over 2,000,000 bytes, or the model service refused the request as too large. | max_bytes (when the byte limit was hit) |
too_many_messages | messages holds more than 500 entries. | max_messages |
prompt_too_large | Desktop. The conversation is too long for the Free plan. | action_url |
Do not retry unchanged. Trim older turns, summarise long history, or move large documents out of the prompt.
429 Too Many Requests
Every 429 carries a Retry-After header and error.retry_after. Wait at least that long.
code | When it happens | Extra fields |
|---|---|---|
rate_limited | More than 600 requests in a minute for one API key (or one signed-in user). | retry_after_seconds; headers X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset-Seconds |
model_busy | The model service is limiting requests. | none |
capacity_busy | Syntic’s capacity manager turned the request away before it was sent. Nothing was charged. Also comes with the X-Capacity-Busy: 1 header. | retry_after_seconds |
concurrent_request | Desktop. Another reply is still running for a Free workspace. | none |
5xx Server errors
| Status | code | When it happens | Retry? |
|---|---|---|---|
| 500 | internal_error | An unexpected failure inside Syntic. | Yes, with back-off. |
| 500 | server_misconfigured | The service is missing configuration. | Yes, later. |
| 502 | upstream_error | The model service failed or rejected the request for a reason that is not yours. | Yes, with back-off. |
| 502 | upstream_unavailable | The model service could not be reached. | Yes, with back-off. |
| 503 | authorization_unavailable | The credential could not be checked just now. | Yes, after Retry-After. |
| 503 | usage_check_unavailable | Your usage could not be checked. Extra fields: limit: "unavailable", resets_at: null. Nothing was charged. | Yes, after Retry-After (5 s). |
| 503 | plan_check_unavailable | Your plan could not be checked. | Yes, after Retry-After. |
| 503 | models_unavailable | The model list could not be read. | Yes, after Retry-After (5 s). |
| 503 | model_retention_unverifiable | Provisional, not in the public catalog. Zero-retention organizations: the model’s retention could not be confirmed, so the request was not sent. Nothing was charged. | Yes, after Retry-After (5 s). |
| 503 | capacity_busy | As for 429 capacity_busy, when the whole service is at capacity. | Yes, after Retry-After. |
| 503 | temporarily_unavailable | Desktop. A Free-plan check could not run. | Yes, after Retry-After. |
| 503 | service_unavailable | Any other short outage. | Yes, after Retry-After. |
| 504 | upstream_timeout | The model took too long to answer. | Yes, with back-off. |
Errors inside a stream
When a streamed response has already started with 200 OK and then breaks, the last event carries the envelope with code: "stream_error" and type: "api_error", then the stream ends:
data: {"error":{"type":"api_error","code":"stream_error","message":"The response stream was interrupted. Retry the request.","param":null,"request_id":"req_6f1c2a9e0b7d4c3a8e5f1b2d"}}Text already received may be incomplete. Retry the whole request. See Stream format.
Handling errors
curl -sS https://app.syntic.ai/v1/chat/completions \
-H "Authorization: Bearer $SYNTIC_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model": "Amara Deep", "messages": []}' \
| jq '.error.type, .error.code, .error.request_id'Retrying safely
- Retry only the types
rate_limit_error,service_unavailable_error,api_errorandtimeout_error, plusfeature_unavailableafter a pause, and a broken stream. - On
429and503, wait at leastRetry-Afterseconds. - Otherwise back off exponentially with jitter (about 1 s, 2 s, 4 s, capped near 30 s) and stop after a few attempts.
- A
4xxother than429fails the same way when sent again unchanged.
Each retried request that produces output is billed on its own.
Codes planned for new endpoints
code | Status | Meaning |
|---|---|---|
beta_required | 400 | A preview endpoint was called without its syntic-beta flag. |
unknown_beta | 400 | The syntic-beta value is not a known flag. |
invalid_version | 400 | syntic-version is not a published version. |
unsupported_parameter | 400 | A field is valid but not supported by the chosen model. |
invalid_parameter | 400 | A field has the wrong type or an out-of-range value. |
idempotency_key_reused | 409 | The same Idempotency-Key was sent with a different body. |
resource_conflict | 409 | The change clashes with the resource’s present state. |
output_schema_violation | 422 | Structured output could not be made to match the schema. |
admin_key_required | 403 | An inference key was used on an /v1/organizations/* endpoint. |
inference_not_allowed | 403 | An Admin API key was used to call a model. |
ip_not_allowed | 403 | The request came from an address outside the key’s IP allowlist. |
region_mismatch | 403 | The request reached a region the workspace’s data may not be processed in. |
policy_denied | 403 | Your organization’s inference policy hook refused the request. |
Endpoint pages list the codes specific to them.
Related pages
- Request IDs
- Rate-limit headers
- Sign in with Syntic: OAuth error codes
- Errors: handling advice