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#

CodeTitleRetryableAction
400Bad RequestNoFix the request body or query params
401UnauthorizedNoCheck your API key
402Payment RequiredNoUpgrade subscription or wait for reset
403ForbiddenNoKey lacks the required scope
404Not FoundNoResource doesn't exist or you don't own it
409ConflictYesWait for the current operation to complete
429Too Many RequestsYesBack off using Retry-After header
500Internal ErrorYesRetry 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.