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
| Field | Always present | Meaning |
|---|---|---|
error | Yes | A short code such as invalid_api_key. Branch on this. |
detail or message | Sometimes | A sentence for humans. Do not parse it; the wording can change. |
| Extra fields | Sometimes | Context for a specific error, such as limit and resets_at on 402, or max_bytes on 413. |
Error codes
400 Bad request
error | Cause | What to do |
|---|---|---|
invalid_json | The body is not valid JSON. | Fix the request body. |
messages_required | messages is missing or empty. | Send at least one message. |
model_unavailable | Your organization uses zero data retention, and the model you asked for needs standard retention. | Choose another model. |
401 Unauthorized
error | Cause | What to do |
|---|---|---|
unauthenticated | No Authorization header. | Send Authorization: Bearer <key>. |
invalid_api_key | No key with that value exists. | Check that the whole key was copied. |
revoked_api_key | The key was revoked. | Create a new key. |
expired_api_key | The key is past its expiry date. | Create a new key. |
invalid_token | A 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"
}
}limit | Meaning |
|---|---|
monthly | The plan’s monthly token allowance is used up. |
session | The rolling 5-hour usage window is full. |
weekly | The weekly usage window is full. |
spend_limit | Usage credits reached the monthly spend limit (the organization’s, or yours as a member). |
no_credits | The plan limit is reached and the usage-credit balance is empty. |
unavailable | Usage could not be checked right now. Retry shortly. |
See Handle usage limits for a pattern that reacts to each case.
403 Forbidden
error | Cause | What to do |
|---|---|---|
public_key_rejected | A pk_live_ public key was sent. | Use a private sk_live_ key. |
origin_not_allowed | The 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_scope | The credential is not allowed to run inference. The body names the missing permission in required. | Use a private key. |
org_membership_required | The key’s owner is no longer an active member of its organization. | Have a current member create a new key. |
account_disabled | The account behind the key is suspended or deleted. | Contact support. |
org_unavailable | The organization is archived or being deleted. | Contact an organization owner. |
405, 413
| Status | error | Cause |
|---|---|---|
| 405 | method_not_allowed | Only POST is accepted. |
| 413 | payload_too_large | The body is larger than about 2 MB. The body includes max_bytes. |
| 413 | too_many_messages | More 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
| Status | error | Cause |
|---|---|---|
| 500 | internal_error | Something failed on Syntic’s side. The body includes a trace_id. |
| 502 | upstream_unavailable | The model service could not be reached. |
| 503 | authorization_unavailable | Your account or organization state could not be checked, so the request was refused. |
| varies | upstream_error | The 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
429after theRetry-Afterdelay, and500,502,503with exponential backoff (for example 1 s, 2 s, 4 s, with some random jitter). Stop after a few attempts. - Do not retry other
4xxerrors unchanged. Fix the request, the key or the account state first. - Back off on
402untilresets_at, or until credits have been added. - Retry
upstream_erroronly when its status is5xx; a4xxthere 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.