Documentation
Errors
Machine-readable codes for the low-level client and curl. Framework drop-ins already normalize the common cases.
Error schema
| Field | Type | Meaning |
|---|---|---|
error | string | Stable label per HTTP status: bad_request, unauthorized, forbidden, not_found, conflict, gone, service_unavailable |
message | string | Human-readable reason for your logs |
code | string | Machine-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):
| Status | Code | Meaning |
|---|---|---|
| 200 | - | Verdict body. Includes store-outage degraded challenges (degraded: true, decision: "challenge"). Not an Error schema response. Official SDKs deserialize this as Verdict. |
| 400 | missing_required_field | action and subject are required |
| 401 | invalid_api_key | Missing or invalid bearer key |
| 403 | raw_pii_not_enabled / insufficient_scope | Raw subject PII needs tenant opt-in, or the key lacks verify:write |
| 409 | idempotency_conflict | Key already associated with a different payload fingerprint: do not reuse; create a new key for the new request |
| 413 | payload_too_large | Request body exceeds the byte cap |
| 415 | unsupported_media_type | Content-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):
| Status | Code | Meaning |
|---|---|---|
| 400 | missing_required_field / invalid_outcome / invalid_value / invalid_observed_at | Request shape problem: fix and resend |
| 401 | invalid_api_key | Missing or invalid bearer key |
| 403 | insufficient_scope | API key lacks feedback:write |
| 404 | unknown_event_id | No matching event for this tenant: attribution bug, fix the verify-time persistence |
| 409 | idempotency_conflict | Key already associated with a different payload fingerprint: do not reuse; create a new key for the new request |
| 413 | payload_too_large | Request body exceeds the byte cap |
| 429 | - | Rate limited: Retry-After present; same Idempotency-Key on retries |
| 503 | feedback_lookup_failed / feedback_ingest_failed | Ledger temporarily unavailable: retry with backoff |
challenge (POST /v1/challenge):
| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_challenge_proof / challenge_method_mismatch / payment_not_succeeded / session_required | Invalid completion, or missing required session |
| 401 | invalid_api_key | Missing or invalid bearer key |
| 403 | insufficient_scope / challenge_session_mismatch | API key lacks challenge:write, or the challenge is bound to a different session |
| 404 | unknown_challenge | Unknown challenge for this tenant |
| 409 | challenge_already_solved | Replay: a challenge clears once |
| 410 | challenge_expired | Challenge expired: issue a fresh one |
| 413 | payload_too_large | Request body exceeds the byte cap |
| 429 | - | Rate limited: Retry-After present; see retry policy below |
| 503 | challenge_issue_failed / challenge_unavailable / payment_verification_unavailable | Temporarily unavailable: retry with backoff |
Retry policy
The official SDKs do not auto-retry. Implement this client-side (or in your queue worker):
| Rule | Policy |
|---|---|
| When to retry | Network errors, 429, and HTTP 5xx Error bodies (feedback/challenge 503). Never retry a verify 200 degraded challenge as transport failure. |
Retry-After | Honor it when present. Wait at least that many seconds before the next attempt (API clamps to 1..60; verify budgets commonly send 15). |
| Backoff | If Retry-After is absent: exponential backoff with full jitter, base ~1s, factor 2, cap ~60s. |
| Max retries | Cap at 3 retries after the first attempt (4 tries total) unless your queue policy is stricter. |
| Idempotency | Same Idempotency-Key (and same payload fingerprint) for the same attempt. Header wins if both header and body are sent. |
| Exhausted budget | On 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):
| Verb | Fingerprint |
|---|---|
verify | SHA-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. |
feedback | SHA-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.