Documentation

Errors

Machine-readable codes for the low-level client and curl. Framework drop-ins already normalize the common cases.

Error schema

FieldTypeMeaning
errorstringStable label per HTTP status: bad_request, unauthorized, forbidden, not_found, conflict, gone, service_unavailable
messagestringHuman-readable reason for your logs
codestringMachine-readable discriminator, stable per failure mode: handle on this, never on the message

Codes by endpoint

Use these tables when you call the API directly (low-level SDK client or curl). Drop-ins already map verify verdicts and degraded paths for you.

verify (POST /v1/verify):

StatusCodeMeaning
200-Verdict body. Includes store-outage degraded challenges (degraded: true, decision: "challenge"). Not an Error schema response. Official SDKs deserialize this as Verdict.
400missing_required_fieldaction and subject are required
401invalid_api_keyMissing or invalid bearer key
403raw_pii_not_enabled / insufficient_scopeRaw subject PII needs tenant opt-in, or the key lacks verify:write
409idempotency_conflictKey already associated with a different payload fingerprint: do not reuse; create a new key for the new request
413payload_too_largeRequest body exceeds the byte cap
415unsupported_media_typeContent-Type must be JSON when a body is sent
429-Rate limited: Retry-After present; see retry policy below. Same Idempotency-Key on retries.

Verify does not return HTTP 503. Store outages are the 200 degraded Verdict above. Feedback and challenge may return 503 Error when their stores are down; that is a different verb. Unexpected verify transport 5xx (if any) are handled by the SDK/integration as HTTP 428 / challenge, never allow.

feedback (POST /v1/feedback):

StatusCodeMeaning
400missing_required_field / invalid_outcome / invalid_value / invalid_observed_atRequest shape problem: fix and resend
401invalid_api_keyMissing or invalid bearer key
403insufficient_scopeAPI key lacks feedback:write
404unknown_event_idNo matching event for this tenant: attribution bug, fix the verify-time persistence
409idempotency_conflictKey already associated with a different payload fingerprint: do not reuse; create a new key for the new request
413payload_too_largeRequest body exceeds the byte cap
429-Rate limited: Retry-After present; same Idempotency-Key on retries
503feedback_lookup_failed / feedback_ingest_failedLedger temporarily unavailable: retry with backoff

challenge (POST /v1/challenge):

StatusCodeMeaning
400invalid_challenge_proof / challenge_method_mismatch / payment_not_succeeded / session_requiredInvalid completion, or missing required session
401invalid_api_keyMissing or invalid bearer key
403insufficient_scope / challenge_session_mismatchAPI key lacks challenge:write, or the challenge is bound to a different session
404unknown_challengeUnknown challenge for this tenant
409challenge_already_solvedReplay: a challenge clears once
410challenge_expiredChallenge expired: issue a fresh one
413payload_too_largeRequest body exceeds the byte cap
429-Rate limited: Retry-After present; see retry policy below
503challenge_issue_failed / challenge_unavailable / payment_verification_unavailableTemporarily unavailable: retry with backoff

Retry policy

The official SDKs do not auto-retry. Implement this client-side (or in your queue worker):

RulePolicy
When to retryNetwork errors, 429, and HTTP 5xx Error bodies (feedback/challenge 503). Never retry a verify 200 degraded challenge as transport failure.
Retry-AfterHonor it when present. Wait at least that many seconds before the next attempt (API clamps to 1..60; verify budgets commonly send 15).
BackoffIf Retry-After is absent: exponential backoff with full jitter, base ~1s, factor 2, cap ~60s.
Max retriesCap at 3 retries after the first attempt (4 tries total) unless your queue policy is stricter.
IdempotencySame Idempotency-Key (and same payload fingerprint) for the same attempt. Header wins if both header and body are sent.
Exhausted budgetOn verify: fail closed to HTTP 428 (never allow), then POST /v1/challenge as needed. On feedback: dead-letter and alert; do not invent an eventId.

Payload fingerprint (what `409 idempotency_conflict` compares):

VerbFingerprint
verifySHA-256 of canonical JSON over { action, session, surface, subject, context }. Object keys sorted; undefined omitted; body idempotencyKey excluded. Key order in the request JSON does not matter.
feedbackSHA-256 of canonical JSON over { eventId, outcome, value, unit, source, observedAt } (unit defaults to usd; source null when omitted; observedAt normalized ISO-8601).

Same key + same fingerprint → replay. Same key + different fingerprint → 409 with code: "idempotency_conflict": create a new key for the new request.

verify attempt 1  (Idempotency-Key: signup:sess_9f3a)
        ↓ 429 / timeout / connection error
wait Retry-After (or backoff + jitter)
        ↓
retry with SAME Idempotency-Key + same payload
        ↓
same decision / response  (or retry until max, then challenge)

Which errors are retryable

Retry only true transport and quota failures: network errors, 429, and HTTP 5xx Error responses (for example feedback/challenge ledger or challenge-store 503). Do not retry a verify 200 degraded challenge as if it were a transport failure: map it like any other challenge verdict (HTTP 428, then POST /v1/challenge).

4xx means fix the request, do not resend it verbatim (except that 429 is retryable per the policy above).

Retries of verify and feedback are safe and deduplicated when you keep the same Idempotency-Key and the same payload fingerprint. On idempotency_conflict, the key is already associated with a different fingerprint: do not reuse the key; generate a new one for the new request.

Without an Idempotency-Key, warehouse dedupe still collapses exact duplicate feedback bodies onto one feedbackId. Equality is the SHA-256 of tenantId + eventId + outcome + value (or null) + unit (default usd) + source (or null) + normalized observedAt (ISO-8601).

observedAt and source are part of identity: two otherwise identical labels with different timestamps or sources are separate observations and will both count if both are accepted. If you omit observedAt, the API fills server receipt time, so two retries a second apart are not duplicates. For connector retries, send a stable business observedAt and prefer an Idempotency-Key.