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';typescriptclass 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
}typescriptUse 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})`);
}
}typescriptHow to Read "Retryable"
The Retryable column below mirrors the SDK's own error.isRetryable flag exactly. Three rules:
- 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.
- Retryable: Yes (
isRetryable === true) means your app may automatically retry the wholemeasureVitals()call with exponential backoff. Only four codes qualify:NETWORK_ERROR,UPLOAD_FAILED,TIMEOUT, andRATE_LIMITED. - Retryable: No means do not build an automated retry loop on this code. Many "No" codes are still user-recoverable — e.g.
FACE_DETECTION_TIMEOUTcan 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
| Code | Description | Retryable |
|---|---|---|
MISSING_API_KEY | No API key was provided — thrown synchronously by new CircadifySDK(...) (and by the React hooks when used outside <CircadifyProvider>), not by measureVitals() | No |
INVALID_API_KEY | API key is malformed, invalid, or revoked (HTTP 401) | No |
BROWSER_NOT_SUPPORTED | Browser lacks navigator.mediaDevices.getUserMedia — the only API this check tests; a missing WebAssembly runtime surfaces as WASM_LOAD_FAILED instead | No |
WASM_LOAD_FAILED | Required SDK runtime assets failed to load | No — fix the network/CSP/CORS issue, then start a new scan |
Camera Errors
| Code | Description | Retryable |
|---|---|---|
CAMERA_PERMISSION_DENIED | The user denied camera access | No |
CAMERA_NOT_AVAILABLE | No camera found on the device | No |
CAMERA_IN_USE | Camera is already in use by another application | No — have the user close the other app, then scan again |
Capture Errors
| Code | Description | Retryable |
|---|---|---|
CAPTURE_FAILED | Frame capture encountered an error; also thrown if a measurement is already in progress on this instance | No |
FACE_NOT_DETECTED | Declared but not currently thrown by 4.x — face absence surfaces as FACE_DETECTION_TIMEOUT instead | No |
FACE_DETECTION_TIMEOUT | The readiness gate (face detection + lighting + stillness + pose) did not pass within 30 seconds — a detected face in poor lighting for 30 seconds also triggers it | No — prompt the user to reposition, improve lighting, and hold still, then scan again |
QUALITY_TOO_LOW | Declared but not currently thrown by 4.x — quality problems surface as quality warnings mid-scan or as PROCESSING_FAILED after upload | No |
CANCELLED | Measurement was cancelled via AbortController, or via sdk.cancel() during the readiness/capture phases (see cancel() for its exact scope) | No |
Network and Processing Errors
| Code | Description | Retryable |
|---|---|---|
NETWORK_ERROR | A network request failed | Yes |
UPLOAD_FAILED | Secure upload failed (server 5xx or transfer failure) | Yes |
TIMEOUT | Capture missed its 60-second frame deadline (thrown even with fallback vitals enabled), or polling for results timed out | Yes |
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.
| Code | HTTP Status | Description | Retryable |
|---|---|---|---|
API_ERROR | 403, other | API returned an error response not covered by a more specific code | No |
QUOTA_EXCEEDED | 429 | Monthly 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_LIMITED | 429 | Sandbox (ck_test_) request rate limit exceeded | Yes — 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_FOUND | 404 | Session ID does not exist or has expired | No |
SESSION_EXPIRED | 410 | Session timed out before completion | No |
PROCESSING_FAILED | 2xx | The 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 |
UNKNOWN | varies | An unexpected error occurred | No |
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);
}
}
}typescriptRetry 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));
}
}
}typescriptBecause 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.'
);
}
}typescriptNever 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
- Events — Monitor errors via event listeners
- Configuration — Including the
allowFallbackVitalssafety documentation - REST API Errors — Server-side error reference