Errors
Every failed call carries a stable code your application can
branch on, plus a human-readable message for logs. The list
below is the full catalog — adding new codes is non-breaking;
existing codes are SCREAMING_SNAKE_CASE and never renamed.
Two failure surfaces
The Face API has two failure shapes, and they look different on the wire:
- Envelope failures — the action ran but reported an error.
HTTP 200, body is the standard envelope with
ok: falseanderrors[]populated. - Transport failures — auth, decode, payload-size, route-not-found.
Non-200 HTTP status, body is
{"detail": "..."}; the action envelope is not built.
Branch on the top-level ok flag for envelope failures, and on the
HTTP status for transport failures.
Catalog
| Code | HTTP | Billed? | Where | Meaning |
|---|---|---|---|---|
INVALID_REQUEST | 400 / envelope | no | both | Bad params (missing required, wrong type, out of range, conflicting fields). |
UNAUTHORIZED | 401 | no | transport | Missing or unknown X-Api-Key. |
FORBIDDEN | 403 | no | transport | Key is valid but doesn't have permission for this action / project. |
PAYMENT_REQUIRED | 402 | no | transport | Project budget exhausted. Allocate more from the wallet. |
PAYLOAD_TOO_LARGE | 413 | no | transport | Image larger than the size cap (decoded). |
RATE_LIMITED | 429 | no | transport | Project's burst or sustained RPS ceiling hit. Back off and retry with jitter. |
IDEMPOTENCY_REPLAY | 200 | no | envelope | Returned when a duplicate requestId lands within the dedup window. Body is the original response; meta.idempotencyReplay = true if meta is enabled. Not billed twice. |
NO_FACE_DETECTED | 200 | no | envelope | Image had no detectable face. |
MULTIPLE_FACES_DETECTED | 200 | no | envelope | Enroll-only — image had more than one face. |
SUBJECT_NOT_FOUND | 200 | no | envelope | subjectId (or externalId on update) doesn't exist in the active project. |
CONFLICT | 200 | no | envelope | Enroll-only — dedup gate hit. See biofrq.enroll for how to bypass. |
INTERNAL | 500 | no | transport | Unexpected server error. Open a ticket with the requestId. |
SERVICE_UNAVAILABLE | 503 | no | transport | Upstream model or DB temporarily unreachable. Retry with backoff. |
Envelope failure shape
{
"ok": false,
"requestId": "2c3f8b1a-9ed3-7c4a-8a1b-...",
"action": "biofrq.enroll",
"data": null,
"errors": [
{
"code": "CONFLICT",
"message": "An existing subject scored above matchThreshold."
}
]
}
Transport failure shape
{
"detail": "Invalid X-Api-Key."
}
message / detail text is human-prose and may change between
releases — do not match on it. Branch on code (envelope) or
HTTP status (transport).
What "not billed" means
If the row in the table above says "no", a code was returned
without debiting micro-credits from your project budget. This
includes RATE_LIMITED (we rejected at the gate) and
IDEMPOTENCY_REPLAY (we already billed the original). It also
includes the input-quality codes like NO_FACE_DETECTED and
PAYLOAD_TOO_LARGE — we don't charge for work that never ran a
model.
A successful envelope (HTTP 200, ok: true) with an empty result
(e.g. identify returning zero candidates, detect returning zero
faces) is billed — the model ran, the answer was "nothing
here."