Circadify

Error Codes

Reference for all Circadify SDK and API error codes.

When something goes wrong, the SDK throws a CircadifyError instance with a machine-readable code, a human-readable message, and a flag indicating whether your application may safely retry the operation.

Error Format

All SDK errors are instances of the CircadifyError class, which extends the native Error. You can import both the class and the error code enum for type-safe handling.

import { CircadifyError, CircadifyErrorCode } from '@circadify/web-sdk';
typescript
class CircadifyError extends Error {
  code: CircadifyErrorCode;             // Machine-readable error code
  message: string;                      // Human-readable description
  isRetryable: boolean;                 // Whether YOUR APP may retry the operation
  details?: Record<string, unknown>;    // Extra context (e.g. retryAfterSeconds on RATE_LIMITED)
  cause?: Error;                        // Underlying error, when available
}
typescript

Use instanceof CircadifyError to distinguish SDK errors from other exceptions:

try {
  const video = document.querySelector('video')!;
  const result = await sdk.measureVitals({ videoElement: video });
} catch (error) {
  if (error instanceof CircadifyError) {
    console.error(`[${error.code}] ${error.message} (retryable: ${error.isRetryable})`);
  }
}
typescript

How to Read "Retryable"

The Retryable column below mirrors the SDK's own error.isRetryable flag exactly. Three rules:

  1. The SDK never retries failed operations internally. Result polling (every 2 seconds, up to 120 seconds) is part of the normal flow, not a retry — every thrown error reaches your code, and any retry is your application's decision.
  2. Retryable: Yes (isRetryable === true) means your app may automatically retry the whole measureVitals() call with exponential backoff. Only four codes qualify: NETWORK_ERROR, UPLOAD_FAILED, TIMEOUT, and RATE_LIMITED.
  3. Retryable: No means do not build an automated retry loop on this code. Many "No" codes are still user-recoverable — e.g. FACE_DETECTION_TIMEOUT can succeed on a fresh scan after the user fixes lighting or framing. Offer a manual "Scan again" button; never loop programmatically. Others (QUOTA_EXCEEDED, INVALID_API_KEY, CANCELLED) will keep failing until an account, configuration, or user decision changes.

SDK Error Codes

These errors originate from the client-side SDK during the measureVitals() lifecycle.

Initialization Errors

CodeDescriptionRetryable
MISSING_API_KEYNo API key was provided — thrown synchronously by new CircadifySDK(...) (and by the React hooks when used outside <CircadifyProvider>), not by measureVitals()No
INVALID_API_KEYAPI key is malformed, invalid, or revoked (HTTP 401)No
BROWSER_NOT_SUPPORTEDBrowser lacks navigator.mediaDevices.getUserMedia — the only API this check tests; a missing WebAssembly runtime surfaces as WASM_LOAD_FAILED insteadNo
WASM_LOAD_FAILEDRequired SDK runtime assets failed to loadNo — fix the network/CSP/CORS issue, then start a new scan

Camera Errors

CodeDescriptionRetryable
CAMERA_PERMISSION_DENIEDThe user denied camera accessNo
CAMERA_NOT_AVAILABLENo camera found on the deviceNo
CAMERA_IN_USECamera is already in use by another applicationNo — have the user close the other app, then scan again

Capture Errors

CodeDescriptionRetryable
CAPTURE_FAILEDFrame capture encountered an error; also thrown if a measurement is already in progress on this instanceNo
FACE_NOT_DETECTEDDeclared but not currently thrown by 4.x — face absence surfaces as FACE_DETECTION_TIMEOUT insteadNo
FACE_DETECTION_TIMEOUTThe readiness gate (face detection + lighting + stillness + pose) did not pass within 30 seconds — a detected face in poor lighting for 30 seconds also triggers itNo — prompt the user to reposition, improve lighting, and hold still, then scan again
QUALITY_TOO_LOWDeclared but not currently thrown by 4.x — quality problems surface as quality warnings mid-scan or as PROCESSING_FAILED after uploadNo
CANCELLEDMeasurement was cancelled via AbortController, or via sdk.cancel() during the readiness/capture phases (see cancel() for its exact scope)No

Network and Processing Errors

CodeDescriptionRetryable
NETWORK_ERRORA network request failedYes
UPLOAD_FAILEDSecure upload failed (server 5xx or transfer failure)Yes
TIMEOUTCapture missed its 60-second frame deadline (thrown even with fallback vitals enabled), or polling for results timed outYes

API Error Codes

These errors originate from the Circadify API and are surfaced by the SDK as CircadifyError instances. For the full server-side error reference, see REST API Errors.

CodeHTTP StatusDescriptionRetryable
API_ERROR403, otherAPI returned an error response not covered by a more specific codeNo
QUOTA_EXCEEDED429Monthly scan quota exhausted on a production (ck_live_) key. Quota is consumed at session start.No — retrying keeps failing until the quota resets or your plan changes
RATE_LIMITED429Sandbox (ck_test_) request rate limit exceededYes — back off, then retry. details.retryAfterSeconds is set when the server supplies a Retry-After header (it currently does not, so apply your own backoff)
SESSION_NOT_FOUND404Session ID does not exist or has expiredNo
SESSION_EXPIRED410Session timed out before completionNo
PROCESSING_FAILED2xxThe server reported the measurement failed (a 2xx response with status: 'failed') or returned an invalid vitals payload. Not produced from 422/500 responses — those surface as API_ERROR (JSON routes) or UPLOAD_FAILED (upload 5xx)No
UNKNOWNvariesAn unexpected error occurredNo
Caution

A 429 response is two different errors: RATE_LIMITED (sandbox throttling — retryable with backoff) and QUOTA_EXCEEDED (monthly scan quota exhausted — never retry; check your Usage page). A retry loop keyed on HTTP status or on "any 429" will hammer the API for a quota that cannot un-exhaust itself. Branch on error.code or trust error.isRetryable, which is true for RATE_LIMITED and false for QUOTA_EXCEEDED.

On Web SDK 4.1.0+, authentication, session, and quota problems that occur during the upload also surface as their precise codes (INVALID_API_KEY, SESSION_EXPIRED, QUOTA_EXCEEDED, …) rather than a generic UPLOAD_FAILED — so UPLOAD_FAILED reliably indicates a genuine transfer or server failure.

Fallback Vitals and Errors

By default, every failure above is thrown to your code — the SDK never substitutes a result for an error. There is one opt-in exception: if you set allowFallbackVitals: true in the SDK configuration, the three transient infrastructure failures (UPLOAD_FAILED, NETWORK_ERROR, TIMEOUT) — only when they occur during the upload/processing phase — resolve with a synthetic placeholder result marked isFallback: true and confidence: 0 instead of throwing. Fallback values are not measurements. The same codes thrown in other phases still throw: the capture watchdog's TIMEOUT (too few frames within 60 seconds) and a NETWORK_ERROR from session start always throw regardless of this setting, as do all other error codes. Leave the option off unless you have read the fallback documentation and your UI explicitly handles isFallback.

Handling Errors

Basic pattern

Wrap measureVitals() in a try/catch and branch on the error code:

import { CircadifySDK, CircadifyError, CircadifyErrorCode } from '@circadify/web-sdk';
 
try {
  const result = await sdk.measureVitals({
    videoElement: document.getElementById('preview') as HTMLVideoElement,
  });
} catch (error) {
  if (!(error instanceof CircadifyError)) throw error;
 
  switch (error.code) {
    case CircadifyErrorCode.CAMERA_PERMISSION_DENIED:
      showMessage('Please allow camera access to measure your vitals.');
      break;
    case CircadifyErrorCode.QUOTA_EXCEEDED:
      showMessage('Monthly scan quota reached. Scans resume next cycle.');
      break;
    case CircadifyErrorCode.CANCELLED:
      // User cancelled — no action needed
      break;
    default:
      if (error.isRetryable) {
        showRetryButton(error.message);
      } else {
        showError(error.message);
      }
  }
}
typescript

Retry with backoff

For retryable errors (isRetryable === true — only NETWORK_ERROR, UPLOAD_FAILED, TIMEOUT, RATE_LIMITED), implement exponential backoff:

async function measureWithRetry(sdk: CircadifySDK, options: MeasurementOptions, maxRetries = 3) {
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      return await sdk.measureVitals(options);
    } catch (error) {
      if (!(error instanceof CircadifyError) || !error.isRetryable || attempt === maxRetries) {
        throw error;
      }
      const delay = Math.min(1000 * Math.pow(2, attempt), 30000);
      await new Promise((resolve) => setTimeout(resolve, delay));
    }
  }
}
typescript

Because this loop gates on isRetryable, it will correctly not retry condition errors (FACE_DETECTION_TIMEOUT, CAMERA_IN_USE, WASM_LOAD_FAILED, …) — those need the user or the environment to change first. Handle them with a manual "Scan again" affordance instead.

Camera permission recovery

When the user denies camera access, guide them to their browser settings:

try {
  const video = document.querySelector('video')!;
  const result = await sdk.measureVitals({ videoElement: video });
} catch (error) {
  if (error instanceof CircadifyError && error.code === CircadifyErrorCode.CAMERA_PERMISSION_DENIED) {
    showInstructions(
      'Camera access is required. Open your browser settings and allow camera access for this site, then refresh the page.'
    );
  }
}
typescript
Caution

Never retry CAMERA_PERMISSION_DENIED, MISSING_API_KEY, INVALID_API_KEY, or QUOTA_EXCEEDED. These require user, configuration, or account changes before they can succeed.

Next Steps