Error Handling
Status codes, error format, and how to handle failures gracefully.
2 min read
Error Response Format#
All non-streaming errors return a consistent JSON envelope:
JSON
{
"success": false,
"error": {
"title": "Not found",
"message": "Deck not found.",
"retryable": false,
"statusCode": 404,
"code": "NOT_FOUND"
}
}
Status Codes#
| Code | Title | Retryable | Action |
|---|---|---|---|
| 400 | Bad Request | No | Fix the request body or query params |
| 401 | Unauthorized | No | Check your API key |
| 402 | Payment Required | No | Upgrade subscription or wait for reset |
| 403 | Forbidden | No | Key lacks the required scope |
| 404 | Not Found | No | Resource doesn't exist or you don't own it |
| 409 | Conflict | Yes | Wait for the current operation to complete |
| 429 | Too Many Requests | Yes | Back off using Retry-After header |
| 500 | Internal Error | Yes | Retry with exponential backoff |
Handling Strategy#
TypeScript
async function callApi(url: string, options: RequestInit) {
const response = await fetch(url, options);
if (response.ok) {
return response.json();
}
const error = await response.json();
const { statusCode, retryable, code } = error.error;
switch (statusCode) {
case 401:
throw new Error("Invalid API key — check your credentials");
case 402:
throw new Error("Quota exceeded — upgrade or wait for daily reset");
case 403:
throw new Error(`Missing scope — your key needs the required scope`);
case 429:
// Retry with backoff
if (retryable) {
const retryAfter = response.headers.get("Retry-After");
await sleep(parseInt(retryAfter ?? "5") * 1000);
return callApi(url, options); // retry once
}
break;
case 500:
if (retryable) {
await sleep(1000);
return callApi(url, options);
}
break;
}
throw new Error(`API error ${statusCode}: ${error.error.message}`);
}
Streaming Errors#
The POST /chat endpoint uses SSE. Errors during streaming are delivered as an error event:
event: error
data: {"type":"error","message":"Stream failed","request_id":"req_abc","retryable":true,"reason":"rate_limit"}
The reason field helps distinguish transient failures (rate_limit, timeout) from permanent ones.
Tip
Always check both the HTTP status code AND the retryable field. A 500 with retryable: true means the server had a transient issue. A 400 with retryable: false means your request needs to be fixed.