Error Handling
Catch and recover from Circadify Python SDK errors across configuration, camera, capture, network, and cancellation paths.
The Python SDK raises CircadifyError for every expected failure — configuration, camera, capture, wire protocol, and cancellation. Wrap your measurement calls in try/except CircadifyError and branch on the subclass or on e.code.
By default the SDK raises the real error and never fabricates vitals. Returning synthetic vitals is opt-in: pass allow_fallback_vitals=True to the constructor and only transient infrastructure failures (NetworkError, UploadFailedError, ScanTimeoutError, ServiceUnavailableError) return a placeholder VitalSigns with is_fallback=True and confidence=0.0 instead of raising. Configuration, camera, capture, quota, and inference (ProcessingFailedError) errors always raise.
The CircadifyError class
Every SDK exception derives from CircadifyError, so a single except CircadifyError catches all of them. Each instance carries a stable string code, a human-readable message, and retry metadata.
| Attribute | Type | Description |
|---|---|---|
code | str | Stable error identifier (e.g. "CAMERA_ERROR"). Branch on this instead of the class name. |
message | str | Human-readable description. |
status | `int | None` |
retryable | bool | True if retrying the same call may succeed. |
retry_after | `float | None` |
from circadify import CircadifyClient, CircadifyError
client = CircadifyClient(api_key="ck_test_your_key_here")
try:
result = client.measure_vitals()
except CircadifyError as e:
print(e.code, e.message, e.status, e.retryable, e.retry_after)pythonCodes are plain strings — there is no CircadifyErrorCode enum. Compare against the literal values in the tables below.
Basic handling
Catch the specific subclass when you want type-specific recovery, or fall back to a code switch and the retryable flag for everything else. Never hardcode a real API key — read it from the environment.
import os
from circadify import (
CircadifyClient,
CircadifyError,
InvalidApiKeyError,
CameraError,
CaptureFailedError,
QuotaExceededError,
ScanCancelledError,
)
client = CircadifyClient(api_key=os.environ["CIRCADIFY_API_KEY"])
try:
result = client.measure_vitals()
print(result.heart_rate, result.confidence)
except InvalidApiKeyError:
print("Check your ck_ API key.")
except CameraError:
print("No usable camera was found.")
except CaptureFailedError:
print("Not enough clean frames — improve lighting and hold still.")
except QuotaExceededError:
print("Account quota exhausted.")
except ScanCancelledError:
pass # user cancelled — nothing to do
except CircadifyError as e:
if e.retryable:
print("Transient failure, safe to retry:", e.message)
else:
print("Unrecoverable:", e.code, e.message)
finally:
client.close()pythonError reference
Every row maps an exception class to its code, whether it is retryable, and what triggered it.
Configuration
| Exception | Code | Retryable | Description |
|---|---|---|---|
MissingApiKeyError | MISSING_API_KEY | No | No API key was passed to CircadifyClient. |
InvalidApiKeyError | API_KEY_INVALID | No | The key is malformed, revoked, or rejected (HTTP 401). |
Dependencies
| Exception | Code | Retryable | Description |
|---|---|---|---|
MissingDependencyError | MISSING_DEPENDENCY | No | An optional extra is missing — install circadify[camera] for capture and the face-glow overlay. |
Camera & capture
| Exception | Code | Retryable | Description |
|---|---|---|---|
CameraError | CAMERA_ERROR | No | The webcam could not be opened or read. |
CaptureFailedError | CAPTURE_FAILED | Yes | Too few usable frames or no face detected during capture. |
Server & wire
| Exception | Code | Retryable | Description |
|---|---|---|---|
ForbiddenError | FORBIDDEN | No | The key is valid but not permitted for this operation (HTTP 403). |
QuotaExceededError | QUOTA_EXCEEDED | No | Account scan quota is exhausted (HTTP 429). |
RateLimitedError | RATE_LIMIT_EXCEEDED | Yes | Request rate limit hit (HTTP 429); honor retry_after. |
SessionNotFoundError | SESSION_NOT_FOUND | No | The referenced session does not exist (HTTP 404). |
SessionExpiredError | SESSION_EXPIRED | No | The session expired before completion (HTTP 410). |
ProcessingFailedError | PROCESSING_FAILED | Yes | Server-side inference failed (HTTP 422). |
UploadFailedError | UPLOAD_FAILED | Yes | The tensor upload did not complete. |
NetworkError | NETWORK_ERROR | Yes | The request never reached the server. |
ServiceUnavailableError | SERVICE_UNAVAILABLE | Yes | The server returned a 5xx response. |
Scan lifecycle
| Exception | Code | Retryable | Description |
|---|---|---|---|
ScanTimeoutError | TIMEOUT | Yes | Result polling exceeded poll_timeout. (Capture/readiness timeouts surface as CaptureFailedError.) |
ScanCancelledError | CANCELLED | No | The scan was cancelled via cancel() or a cancel_event. |
Retry with backoff
Retry only when e.retryable is True. Prefer the server-provided e.retry_after when present; otherwise back off exponentially.
import time
from circadify import CircadifyClient, CircadifyError
def measure_with_retry(client: CircadifyClient, max_retries: int = 3):
for attempt in range(max_retries):
try:
return client.measure_vitals()
except CircadifyError as e:
if not e.retryable or attempt == max_retries - 1:
raise
delay = e.retry_after if e.retry_after is not None else 2 ** attempt
time.sleep(delay)
raise RuntimeError("unreachable")pythonNever auto-retry API-key, quota, or permission errors (MISSING_API_KEY, API_KEY_INVALID, FORBIDDEN, QUOTA_EXCEEDED). Their retryable flag is False because they require a configuration or account change, not another attempt — looping on them just burns time and noise.
Next Steps
- Methods — Measurement methods that raise these errors
- Camera & Input — Avoid camera and capture errors at the source
- REST Client — The raw four-call wire path and its errors
- REST API Errors — Server-side error reference