Circadify

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
FieldTypeDescription
errorstringMachine-readable error code
messagestringHuman-readable description
retryablebooleanWhether 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)

CodeDescriptionResolution
API_KEY_INVALIDThe API key is missing, malformed, or revokedCheck your key in the developer dashboard. Create a new key if needed.
UNAUTHORIZEDBearer token is missing or invalidRe-authenticate and obtain a fresh token.
Tip

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)

CodeDescriptionResolution
FORBIDDENYour developer account is not active, or the session's environment (live/test) does not match the calling keyCheck your account status in the portal; use the same key environment the session was created with.
DEVELOPER_SUSPENDEDYour 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)

CodeDescriptionResolution
INVALID_REQUESTMissing or invalid request parametersCheck the request body and path parameters against the endpoint documentation.
VERIFICATION_TOKEN_INVALIDEmail verification or password reset token is invalid or expiredRequest a new verification email or password reset.

Not Found (404)

CodeDescriptionResolution
SESSION_NOT_FOUNDThe session ID does not existSessions expire after a short time. Create a new session.
DEVELOPER_NOT_FOUNDNo developer account found for this identifierCheck the account exists and the ID is correct.

Conflict (409)

CodeDescriptionResolution
INVALID_REQUESTThe 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)

CodeDescriptionResolution
SESSION_EXPIREDThe session timed out before completionCreate a new session and retry the measurement.
— (result purged)A completed result passed the 24-hour retention window; its vitals were purgedResults 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)

CodeDescriptionResolution
RATE_LIMIT_EXCEEDEDSandbox (ck_test_) request rate limit exceeded (best-effort)Retryable. Back off with exponential delay and retry. See Rate Limits.
QUOTA_EXCEEDEDMonthly scan quota exhausted on a production (ck_live_) keyNot retryable this month. Check the Usage page in the developer portal.

Server Errors (500)

CodeDescriptionResolution
INTERNAL_ERRORAn unexpected server error occurredThe 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

StatusMeaning
200Success
201Created (e.g., new API key)
202Accepted — upload-complete acknowledged, or result poll still in progress
204No Content — proxy upload succeeded
400Bad Request — invalid parameters
401Unauthorized — invalid or missing credentials
403Forbidden — account not active, or session/key environment mismatch
404Not Found — resource does not exist (or belongs to another developer)
409Conflict — resource state conflict
410Gone — resource has expired
413Payload Too Large — proxy upload body exceeds the limit
415Unsupported Media Type — proxy upload content-type not allowed
422Unprocessable Entity — uploaded measurement could not be processed
429Rate Limited — too many requests
500Internal Error — server-side failure
503Service 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));
  }
}
typescript

Recommended parameters:

ParameterValue
Max retries3
Initial delay1 second
Max delay30 seconds
Backoff multiplier2x
Caution

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