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:

  1. Envelope failures — the action ran but reported an error. HTTP 200, body is the standard envelope with ok: false and errors[] populated.
  2. 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

CodeHTTPBilled?WhereMeaning
INVALID_REQUEST400 / envelopenobothBad params (missing required, wrong type, out of range, conflicting fields).
UNAUTHORIZED401notransportMissing or unknown X-Api-Key.
FORBIDDEN403notransportKey is valid but doesn't have permission for this action / project.
PAYMENT_REQUIRED402notransportProject budget exhausted. Allocate more from the wallet.
PAYLOAD_TOO_LARGE413notransportImage larger than the size cap (decoded).
RATE_LIMITED429notransportProject's burst or sustained RPS ceiling hit. Back off and retry with jitter.
IDEMPOTENCY_REPLAY200noenvelopeReturned 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_DETECTED200noenvelopeImage had no detectable face.
MULTIPLE_FACES_DETECTED200noenvelopeEnroll-only — image had more than one face.
SUBJECT_NOT_FOUND200noenvelopesubjectId (or externalId on update) doesn't exist in the active project.
CONFLICT200noenvelopeEnroll-only — dedup gate hit. See biofrq.enroll for how to bypass.
INTERNAL500notransportUnexpected server error. Open a ticket with the requestId.
SERVICE_UNAVAILABLE503notransportUpstream model or DB temporarily unreachable. Retry with backoff.

Envelope failure shape

json
{
  "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

json
{
  "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."