One envelope for every errorLink to this section
The official error object stays first: type, code, message, and the offending param. Commercial typing rides beside it, so a client can decide what to do without parsing prose.
{
"code": "invalid_api_key",
"error": {
"code": "invalid_api_key",
"message": "Invalid API key.",
"param": null,
"type": "invalid_api_key"
},
"next_action": null,
"retry_after_seconds": null,
"retryable": false,
"summary": "Invalid API key."
}
{
"code": "chat_completions_retired",
"error": {
"code": "chat_completions_retired",
"message": "POST /v1/chat/completions is retired. Use POST /v1/responses.",
"param": null,
"type": "chat_completions_retired"
},
"summary": "POST /v1/chat/completions is retired. Use POST /v1/responses.",
"retryable": false
}
error.codeis the stable identifier. Branch on this, never on the message text.error.messageis for humans, anderror.paramnames the field to fix when the failure is about your request.summaryis a short sentence safe to log or surface;retryablesays whether an identical retry can succeed after the advertised delay.next_actionstates what to change;retry_after_secondsis the delay to honor when retrying. Rate-limit denials also send theRetry-Afterheader.- Responses carry an
x-request-idheader. Quote it in support requests — it identifies the exact call.
Status codesLink to this section
Status classes are stable. The code inside the envelope tells you what actually happened.
| 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 catalog, 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 honored 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 honest not-ready, not a silent downgrade. |
503 | Required capacity, catalog, credentials, or the model route is temporarily unavailable. |
Codes you will meetLink to this section
Each code names the next action. This is the set a first integration can hit; the contract carries the full wire.
| code | Status | What to do |
|---|---|---|
invalid_api_key | 401 | Check the key: it must start with sk-sx-, be unrevoked, and be sent as Authorization: Bearer …. |
invalid_request | 400 | Fix the field named in error.param. The message states what the protocol refused. |
model_not_found | 404 | Copy an id from the catalog; an id we do not sell is never substituted silently. |
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. The in-flight attempt is still running. |
idempotency_key_reuse | 422 | Use a new Idempotency-Key for a new request body. A key identifies one request. |
idempotency_outcome_unknown | 409 | The original execution is unresolved and will not be repeated. Keep the reference and reconcile with GET or an idempotency replay; do not resend with a new key. |
rate_limit_exceeded | 429 | Reduce rate or in-flight requests, then retry after retry_after_seconds / Retry-After. |
model_unavailable | 503 | Retry later or choose another catalog model. This is a route or capacity failure, never a request-shape problem. |
file_not_found | 400 | Re-upload the file and use the new file id; ids expire with the seven-day retention window. |
response_not_cancellable | 400 | Responses are not background jobs. Retrieve the response instead of cancelling it. |
chat_completions_retired | 404 | Move the call to POST /responses — the message names the replacement path. |
shared_provision_retired | 404 | Machine keys are minted per tenant with POST /v1/admin/tenants/{org}/keys — see platform keys. |
Retries are safe by keyLink to this section
POST /responses is the call to protect, and one header does it. A UUID Idempotency-Key binds one key to one exact request body.
curl https://api.models.sylphx.ai/v1/responses \
-H "Authorization: Bearer $SYLPHX_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 8f8d57c8-8868-4d90-90cb-55e892cb80a5" \
-d '{
"model": "openai/gpt-5.5",
"input": "Summarise this incident report in three bullets."
}'
What happens on each retry
- Identical retry after completion —
200with the original response and the headeridempotency-replayed: true. No new generation, no second charge. - Same key, different body — typed
422 idempotency_key_reuse. The key is spent; use a fresh one. - Same key while the first attempt runs —
409 idempotency_in_progress. Wait for the first call, then resend the same bytes with the same key. - Replay window — a completed response can be replayed for seven days from completion. After that the key may identify a new request.
- Unresolved outcomes — if an execution outcome is unknown, the API reports it (
idempotency_outcome_unknown, orresponse_commit_outcome_unknownwith the original response reference) instead of pretending the call failed and running it again.
retryable is true, or on a network failure before you read a response. Do not reuse a key for a changed body — and do not build retry loops that ignore retry_after_seconds.Rate limitsLink to this section
Limits are per key, enforced before a request reaches a model. Keys minted in the console or by a platform default to the pro tier, which is not subject to the free-tier envelope.
- Free-tier envelope — one envelope across every model on that key: 300 burst requests per minute, 180 sustained requests per minute, and 32 requests in flight.
- Headers on the response —
x-ratelimit-limit,x-ratelimit-remaining, andx-ratelimit-reset(Unix seconds when the window resets). - On a denial —
429 rate_limit_exceeded, aRetry-Afterheader, andretry_after_seconds/next_actionin the envelope. Concurrency denials tell you to reduce in-flight requests; rate denials tell you to slow the request rate. - Provider throttling — if the serving provider throttles after failover, you receive the same typed
429with the provider’s retry hint, not a misleading model error.
A retry helperLink to this section
The shape every production client needs: a stable key per logical request, retry only what the envelope marks retryable, and honor the advertised delay.
import { randomUUID } from "node:crypto";
async function createResponse(body: unknown, key = randomUUID()) {
const attempt = () =>
fetch(`https://api.models.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),
});
for (let i = 0; i < 5; i += 1) {
const response = await attempt(); // a network failure retries the same key
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 helpLink to this section
Support needs four facts to reproduce a call. All four are in the response you already have.
- The
x-request-idheader (and the UTC time of the call). - The
error.codeandsummaryvalues. - The model id you sent and whether the call streamed.
- The body you sent, with the key redacted.
Write to [email protected] or continue with the quickstart and the API contract.