---
title: AI errors and retries
description: The one error envelope, the status and error codes, idempotent retries, rate limits and what support needs.
type: reference
product: ai
summary: Codes, idempotency and limits for the AI API
updated: 2026-09-30
order: 5
---

Every failure returns one JSON envelope. The official error object stays
first — `type`, `code`, `message` and the offending `param` — and typed fields
ride beside it, so a client can decide what to do without parsing prose.

```json
{
  "code": "invalid_api_key",
  "error": {
    "code": "invalid_api_key",
    "message": "Invalid API key.",
    "param": null,
    "type": "invalid_api_key"
  },
  "summary": "Invalid API key.",
  "retryable": false,
  "retry_after_seconds": null,
  "next_action": null
}
```

- `error.code` is the stable identifier. Branch on it, never on the message.
- `error.param` names the field to fix when the failure is about your request.
- `summary` is a short sentence safe to log; `retryable` says whether an
  identical retry can succeed after the advertised delay.
- `next_action` states what to change; `retry_after_seconds` is the delay to
  honour. A rate-limit denial also sends the `Retry-After` header.
- Responses carry an `x-request-id` header. Quote it in support requests.

## Status codes

| Status | Meaning |
| --- | --- |
| `400` | The request is invalid, violates the public protocol, or exceeds the model's context window. |
| `401` | The bearer key is missing, malformed or revoked. |
| `404` | The endpoint does not exist, the model is not in the catalogue, or the stored object is not visible to your key. |
| `409` | An identical idempotent request is still in progress, or its outcome is unresolved. |
| `413` | The request body exceeds the bounded input limit. |
| `422` | The request cannot be honoured under public rules with the meaning you asked for, or an `Idempotency-Key` was reused with a different body. |
| `429` | Rate, concurrency or spend limits refused the call, or the serving provider throttled after failover. |
| `501` | A documented facade or hosted tool type is not implemented yet — an explicit not-ready, never a silent downgrade. |
| `503` | Required capacity, catalogue, credentials or the model route is temporarily unavailable. |

## Codes you will meet

| Code | Status | What to do |
| --- | --- | --- |
| `invalid_api_key` | 401 | The key must start with `sylphx_sk_`, be unrevoked, and be sent as `Authorization: Bearer …`. |
| `invalid_request` | 400 | Fix the field named in `error.param`. |
| `model_not_found` | 404 | Copy an id from [the catalogue](/ai/models); an id we do not sell is never substituted. |
| `context_length_exceeded` | 400 | Shorten the input, or compact the conversation and continue from the compacted window. |
| `invalid_idempotency_key` | 400 / 413 | Send a non-empty key of at most 256 bytes; a UUID is the intended shape. |
| `idempotency_in_progress` | 409 | Wait, then resend the identical body with the same key. |
| `idempotency_key_reuse` | 422 | Use a new key for a new request body. |
| `idempotency_outcome_unknown` | 409 | The original execution is unresolved and will not be repeated. Reconcile with a GET or an idempotency replay; do not resend under a new key. |
| `rate_limit_exceeded` | 429 | Reduce rate or in-flight requests, then retry after `retry_after_seconds`. |
| `model_unavailable` | 503 | Retry later or choose another model. A route or capacity failure, never a request-shape problem. |
| `file_not_found` | 400 | Re-upload the file and use the new file id. |
| `response_not_cancellable` | 400 | Responses are not background jobs. Retrieve the response instead. |

## Retries are safe by key

`POST /responses` is the call to protect. A UUID `Idempotency-Key` binds one
key to one exact request body.

```bash
curl https://api.sylphx.ai/v1/responses \
  -H "Authorization: Bearer $SYLPHX_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 00000000-0000-4000-8000-000000000000" \
  -d '{"model": "openai/gpt-5.5", "input": "Summarise this incident report in three bullets."}'
```

- Identical retry after completion: `200` with the original response and the
  header `idempotency-replayed: true`. No new generation.
- Same key, different body: `422 idempotency_key_reuse`. The key is spent.
- Same key while the first attempt runs: `409 idempotency_in_progress`.
- A completed response can be replayed for seven days from completion.
- If an execution outcome is unknown, the API says so
  (`idempotency_outcome_unknown`, or `response_commit_outcome_unknown` with the
  original response reference) instead of pretending the call failed.

<Callout tone="warning" title="Retry only when it is safe">
	Retry an identical body with the same key when `retryable` is true, or on a
	network failure before you read a response. Never reuse a key for a changed
	body, and never ignore `retry_after_seconds`.
</Callout>

## Rate limits

Limits are per key and enforced before a request reaches a model. Free-tier
keys share one envelope across every model: 300 burst requests per minute, 180
sustained requests per minute and 32 requests in flight. A response carries
`x-ratelimit-limit`, `x-ratelimit-remaining` and `x-ratelimit-reset` (Unix
seconds). A denial is `429 rate_limit_exceeded` with `Retry-After`; concurrency
denials tell you to reduce in-flight requests, rate denials to slow down. If
the serving provider throttles after failover you receive the same typed `429`
with the provider's retry hint.

## A retry helper

```ts
import { randomUUID } from 'node:crypto'

async function createResponse(body: unknown, key = randomUUID()) {
  for (let i = 0; i < 5; i += 1) {
    const response = await fetch('https://api.sylphx.ai/v1/responses', {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${process.env.SYLPHX_API_KEY}`,
        'Content-Type': 'application/json',
        'Idempotency-Key': key,
      },
      body: JSON.stringify(body),
    })
    if (response.ok) return response.json()
    const payload = await response.json()
    if (payload.retryable !== true) throw new Error(`${payload.code}: ${payload.summary}`)
    const wait = payload.retry_after_seconds ?? 2 ** i
    await new Promise((resolve) => setTimeout(resolve, wait * 1000))
  }
  throw new Error('retries exhausted')
}
```

## If you need help

Support needs four facts, all in the response you already have: the
`x-request-id` header and the UTC time of the call; the `error.code` and
`summary`; the model id and whether the call streamed; and the body you sent
with the key redacted. Send them through [contact](/contact).
