BuildErrors

Errors

When the Syntic API cannot complete a request it answers with a non-200 HTTP status and a small JSON body. Branch on the error field: it is a stable, machine-readable code. Some errors add a human-readable detail or message, and a few add extra fields.

{
  "error": {
    "type": "authentication_error",
    "code": "revoked_api_key",
    "message": "This API key was revoked. Create a new one in Settings → API keys.",
    "param": null,
    "request_id": "req_6f1c2a9e0b7d4c3a8e5f1b2d"
  }
}

Every error has the same envelope: error.type (the category, fixed by the HTTP status) and error.code (the specific reason), plus message, param and request_id. Branch on code; fall back to type for codes you do not know.

Almost every response, including most errors, carries an x-syntic-request-id header. Log it next to the error; support can trace a request from that id. The two exceptions are 405 and 500: a 500 body carries a trace_id instead, which serves the same purpose.

Error shape at a glance

FieldAlways presentMeaning
errorYesA short code such as invalid_api_key. Branch on this.
detail or messageSometimesA sentence for humans. Do not parse it; the wording can change.
Extra fieldsSometimesContext for a specific error, such as limit and resets_at on 402, or max_bytes on 413.

Error codes

400 Bad request

errorCauseWhat to do
invalid_jsonThe body is not valid JSON.Fix the request body.
messages_requiredmessages is missing or empty.Send at least one message.
model_unavailableYour organization uses zero data retention, and the model you asked for needs standard retention.Choose another model.

401 Unauthorized

errorCauseWhat to do
unauthenticatedNo Authorization header.Send Authorization: Bearer <key>.
invalid_api_keyNo key with that value exists.Check that the whole key was copied.
revoked_api_keyThe key was revoked.Create a new key.
expired_api_keyThe key is past its expiry date.Create a new key.
invalid_tokenA Syntic Code sign-in token could not be verified.Sign in again.

402 Payment required

quota_exhausted means your plan allowance is used up and usage credits could not cover the request. The body explains which limit was reached and when it resets:

{
  "error": {
    "type": "insufficient_quota",
    "code": "quota_exhausted",
    "message": "You've hit your weekly usage limit. ...",
    "param": null,
    "request_id": "req_6f1c2a9e0b7d4c3a8e5f1b2d",
    "limit": "weekly",
    "resets_at": "2026-10-13T12:00:00Z",
    "upgrade_url": "https://syntic.ai/pricing",
    "add_credit_url": "https://syntic.ai/agents/billing#api-topup"
  }
}
limitMeaning
monthlyThe plan’s monthly token allowance is used up.
sessionThe rolling 5-hour usage window is full.
weeklyThe weekly usage window is full.
spend_limitUsage credits reached the monthly spend limit (the organization’s, or yours as a member).
no_creditsThe plan limit is reached and the usage-credit balance is empty.
unavailableUsage could not be checked right now. Retry shortly.

See Handle usage limits for a pattern that reacts to each case.

403 Forbidden

errorCauseWhat to do
public_key_rejectedA pk_live_ public key was sent.Use a private sk_live_ key.
origin_not_allowedThe key has an origin allowlist and the request’s origin is missing or not on it.Call from an allowed origin, or clear the list for server use.
insufficient_scopeThe credential is not allowed to run inference. The body names the missing permission in required.Use a private key.
org_membership_requiredThe key’s owner is no longer an active member of its organization.Have a current member create a new key.
account_disabledThe account behind the key is suspended or deleted.Contact support.
org_unavailableThe organization is archived or being deleted.Contact an organization owner.

405, 413

StatuserrorCause
405method_not_allowedOnly POST is accepted.
413payload_too_largeThe body is larger than about 2 MB. The body includes max_bytes.
413too_many_messagesMore than 500 messages. The body includes max_messages.

429 Too many requests

You sent more than 600 requests in the last 60 seconds with the same key (or, for Syntic Code sign-ins, the same user). The window slides, so capacity frees up gradually rather than all at once.

{
  "error": {
    "type": "rate_limit_error",
    "code": "rate_limited",
    "message": "Too many requests. Retry shortly.",
    "param": null,
    "request_id": "req_6f1c2a9e0b7d4c3a8e5f1b2d",
    "retry_after": 12,
    "retry_after_seconds": 12
  }
}

Detect it by error.type rate_limit_error (or the 429 status). Read the Retry-After header (seconds) first; the response also has X-RateLimit-Limit, X-RateLimit-Remaining (0) and X-RateLimit-Reset-Seconds. These headers are sent only on this 429, never on a successful response. See Limits.

5xx Server errors

StatuserrorCause
500internal_errorSomething failed on Syntic’s side. The body includes a trace_id.
502upstream_unavailableThe model service could not be reached.
503authorization_unavailableYour account or organization state could not be checked, so the request was refused.
variesupstream_errorThe model service rejected or failed the request. The status is passed through.

Errors inside a stream

When a streamed reply fails after it has started, the status is already 200. Instead you receive a final event data: {"error":{"type":"api_error","code":"stream_error","message":"The response stream was interrupted. Retry the request.","param":null,"request_id":"req_6f1c2a9e0b7d4c3a8e5f1b2d"}} and the stream closes. See Streaming.

Errors in OpenAI-compatible SDKs

OpenAI-compatible client SDKs turn a non-200 response into an exception that holds the status code and the parsed body. Because Syntic’s error field is a string rather than an object, read the code from the raw body:

import openai
 
try:
    reply = client.chat.completions.create(model="Amara S1.2", stream=False, messages=messages)
except openai.APIStatusError as e:
    body = e.body if isinstance(e.body, dict) else {}
    code = (body.get("error") or {}).get("code")
    request_id = e.response.headers.get("x-syntic-request-id")
    print(e.status_code, code, request_id)

The SDKs retry some failures (such as 429 and 5xx) a couple of times on their own. Set max_retries (Python) or maxRetries (Node.js) when you want to control this yourself.

Retrying safely

The full retry table, backoff timing and a helper are on Rate limits & retries.

  • Retry 429 after the Retry-After delay, and 500, 502, 503 with exponential backoff (for example 1 s, 2 s, 4 s, with some random jitter). Stop after a few attempts.
  • Do not retry other 4xx errors unchanged. Fix the request, the key or the account state first.
  • Back off on 402 until resets_at, or until credits have been added.
  • Retry upstream_error only when its status is 5xx; a 4xx there usually means the request itself was rejected (for example, it is longer than the context window).

A small helper that follows these rules:

import random, time
import openai
 
RETRYABLE = {429, 500, 502, 503}
 
def create_with_retry(client, attempts=4, **kwargs):
    for attempt in range(attempts):
        try:
            return client.chat.completions.create(**kwargs)
        except openai.APIStatusError as e:
            if e.status_code not in RETRYABLE or attempt == attempts - 1:
                raise
            wait = e.response.headers.get("retry-after")
            delay = float(wait) if wait else (2 ** attempt) + random.random()
            time.sleep(delay)

Tell support about a problem

Include the x-syntic-request-id (or trace_id for a 500), the time of the request, the model you asked for and the error code. Never include your API key.

Was this page helpful?