Errors
Handle Circadify REST API errors — structured error responses, error codes, and retry strategies.
The Circadify API uses standard HTTP status codes and returns structured error responses to help you diagnose and handle failures.
Error Response Format
Most API errors follow this structure:
{
"error": "RATE_LIMIT_EXCEEDED",
"message": "Rate limit exceeded",
"retryable": true
}json| Field | Type | Description |
|---|---|---|
error | string | Machine-readable error code |
message | string | Human-readable description |
retryable | boolean | Whether the request can be retried |
Rate-limited and quota responses use HTTP 429 with the body above; the API does not emit Retry-After or X-RateLimit-* headers, so apply your own backoff.
Session result failures return the session status plus an error message so SDKs can map the failure to their client-side error type.
Error Codes
Authentication Errors (401)
| Code | Description | Resolution |
|---|---|---|
API_KEY_INVALID | The API key is missing, malformed, or revoked | Check your key in the developer dashboard. Create a new key if needed. |
UNAUTHORIZED | Bearer token is missing or invalid | Re-authenticate and obtain a fresh token. |
All API key validation failures return the same API_KEY_INVALID code regardless of the specific reason (missing, revoked, or malformed — keys have no expiration date). This prevents attackers from enumerating valid keys. One carve-out: a valid key whose developer account is not active returns 403 DEVELOPER_SUSPENDED instead.
Authorization Errors (403)
| Code | Description | Resolution |
|---|---|---|
FORBIDDEN | Your developer account is not active, or the session's environment (live/test) does not match the calling key | Check your account status in the portal; use the same key environment the session was created with. |
DEVELOPER_SUSPENDED | Your account is not active (suspended, or not yet approved) | Contact support to resolve. |
Accessing a session that belongs to another developer does not return 403 — it returns 404 SESSION_NOT_FOUND, so cross-tenant session existence is hidden. A session you don't own looks like a 404. There is no DEVELOPER_PENDING or DEVELOPER_NOT_VERIFIED error: a pending account gets 403 DEVELOPER_SUSPENDED, and an unverified email surfaces as 401 UNAUTHORIZED.
Request Errors (400)
| Code | Description | Resolution |
|---|---|---|
INVALID_REQUEST | Missing or invalid request parameters | Check the request body and path parameters against the endpoint documentation. |
VERIFICATION_TOKEN_INVALID | Email verification or password reset token is invalid or expired | Request a new verification email or password reset. |
Not Found (404)
| Code | Description | Resolution |
|---|---|---|
SESSION_NOT_FOUND | The session ID does not exist | Sessions expire after a short time. Create a new session. |
DEVELOPER_NOT_FOUND | No developer account found for this identifier | Check the account exists and the ID is correct. |
Conflict (409)
| Code | Description | Resolution |
|---|---|---|
INVALID_REQUEST | The session is no longer accepting uploads (already processing or terminal) | Don't re-upload after upload-complete; create a new session if needed. |
Signup does not emit an email-conflict error — POST /developer/signup returns the same generic 201 response whether or not the email already exists, to prevent email enumeration.
Gone (410)
| Code | Description | Resolution |
|---|---|---|
SESSION_EXPIRED | The session timed out before completion | Create a new session and retry the measurement. |
| — (result purged) | A completed result passed the 24-hour retention window; its vitals were purged | Results must be fetched within 24 hours of completion — persist them on your side. Run a new scan if a fresh reading is needed. |
Processing Failed (422)
When processing fails, the result poll returns 422 with { "session_id", "status": "failed", "error": "<message>" } — no error envelope. failed is a session status, not an error code. Start a new measurement and retry under better scan conditions.
Rate Limiting & Quota (429)
| Code | Description | Resolution |
|---|---|---|
RATE_LIMIT_EXCEEDED | Sandbox (ck_test_) request rate limit exceeded (best-effort) | Retryable. Back off with exponential delay and retry. See Rate Limits. |
QUOTA_EXCEEDED | Monthly scan quota exhausted on a production (ck_live_) key | Not retryable this month. Check the Usage page in the developer portal. |
Server Errors (500)
| Code | Description | Resolution |
|---|---|---|
INTERNAL_ERROR | An unexpected server error occurred | The envelope reports retryable: false — don't loop on it automatically. A manual retry after a pause may still succeed; if persistent, contact support. |
There is no SERVICE_UNAVAILABLE error code, and INFERENCE_FAILED is not emitted in the session flow — a processing (inference) failure surfaces on the result poll as 422 with { "session_id", "status": "failed", "error": "<message>" }.
HTTP Status Code Summary
| Status | Meaning |
|---|---|
200 | Success |
201 | Created (e.g., new API key) |
202 | Accepted — upload-complete acknowledged, or result poll still in progress |
204 | No Content — proxy upload succeeded |
400 | Bad Request — invalid parameters |
401 | Unauthorized — invalid or missing credentials |
403 | Forbidden — account not active, or session/key environment mismatch |
404 | Not Found — resource does not exist (or belongs to another developer) |
409 | Conflict — resource state conflict |
410 | Gone — resource has expired |
413 | Payload Too Large — proxy upload body exceeds the limit |
415 | Unsupported Media Type — proxy upload content-type not allowed |
422 | Unprocessable Entity — uploaded measurement could not be processed |
429 | Rate Limited — too many requests |
500 | Internal Error — server-side failure |
503 | Service Unavailable — temporary backend issue |
Retry Strategy
For retryable errors (retryable: true), implement exponential backoff:
async function fetchWithRetry(url: string, options: RequestInit, maxRetries = 3) {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
const response = await fetch(url, options);
if (response.ok) return response;
const body = await response.json();
// Honor the `retryable` flag: don't retry non-retryable client errors.
// Note a 429 can be QUOTA_EXCEEDED (retryable: false) — don't retry it.
if (response.status < 500 && body.retryable === false) {
throw new Error(body.message);
}
if (attempt === maxRetries) {
throw new Error(body.message);
}
// No Retry-After header is emitted — use exponential backoff
const delay = Math.min(1000 * Math.pow(2, attempt), 30000);
await new Promise(resolve => setTimeout(resolve, delay));
}
}typescriptRecommended parameters:
| Parameter | Value |
|---|---|
| Max retries | 3 |
| Initial delay | 1 second |
| Max delay | 30 seconds |
| Backoff multiplier | 2x |
Never retry 401 or 403 errors — these indicate a credentials or permissions issue that will not resolve with retries. A 429 may be RATE_LIMIT_EXCEEDED (sandbox, retryable) or QUOTA_EXCEEDED (production quota, not retryable) — branch on the retryable flag. No Retry-After header is emitted, so use your own backoff.
Next Steps
- Rate Limits — Understand and manage rate limits
- Sessions — Session endpoints and lifecycle
- SDK Error Codes — Client-side error reference