Errors & idempotency

How the API reports failures, what each status and machine code means, and how to make create requests safely retryable with idempotency keys.

Error format

Every error returns a single, uniform JSON envelope. Branch on the machine-readable error field, not on the human-readable message — and log the request_id so support can trace the call.

{
  "error": "<code>",        // stable, machine-readable code
  "message": "…",          // human-readable explanation
  "status": 422,           // HTTP status, mirrored in the body
  "request_id": "req_…"      // correlate with support / logs
}

For example, reusing an Idempotency-Key with a different body returns:

{
  "error": "idempotency_key_reuse",
  "message": "Idempotency-Key was reused with a different request body.",
  "status": 422,
  "request_id": "req_8sZc1pQ2"
}
Validation errors

Request-schema failures (missing or malformed fields) surface as 422 with details about the offending field(s). Treat error: "validation_error" as the generic schema-validation code.

HTTP status codes

The API uses conventional HTTP semantics. The status is also mirrored inside the error envelope.

StatusMeaningWhen
200 OKSuccessRead succeeded, or a write that returns a body (e.g. /authorize).
201 CreatedResource createdA Principal, Agent, Mandate, webhook, Client, or key was created.
202 AcceptedPending verificationA Principal registered in platform_provider mode — verification is pending; the body carries a verification_session.
204 No ContentSuccess, empty bodyA webhook was deleted.
400 Bad RequestMalformed requestThe request is structurally invalid (e.g. no fields provided to update, unknown outcome value).
401 UnauthorizedMissing/invalid keyNo credential, or a credential that does not authenticate (missing X-API-Key / X-Admin-Key).
403 ForbiddenNot allowedAuthenticated but not permitted: wrong scope, IP not allowlisted, cross-tenant access, a sandbox-only action on live, or an invalid admin key.
404 Not FoundNo such resourceThe Principal, Agent, Mandate, webhook, Client, or credential does not exist (or isn't yours).
409 ConflictState conflictA uniqueness constraint was violated — e.g. a duplicate client_customer_ref.
422 UnprocessableValidation / business ruleThe request parsed but failed schema or a business rule (e.g. a Program you don't own, a bad phone format).
429 Too Many RequestsRate limitedYou exceeded a rate limit. Back off and retry (see below).
5xx Server ErrorServer-side faultAn unexpected error. Safe to retry with backoff. Note: /authorize is designed never to 500 — it fails safe to a decision.

Error codes

The error field carries a stable machine code. The onboarding API in particular maps its business rules to these codes; build your error handling against them.

CodeStatusMeaning
program_not_owned422A program_id in the request does not belong to the calling Client.
duplicate_client_customer_ref409The client_customer_ref already exists for this Client — refs are unique per tenant.
principal_not_in_program422An agent's program_id isn't one the Principal is enrolled in, or program_id is required because the Principal is in 0 or >1 Programs.
principal_not_found404The Principal (or the agent's Principal) was not found for this Client.
invalid_phone_format422SMS step-up was requested but no valid E.164 phone (step_up.phone or profile.phone) was provided.
attestation_required422verification.mode is client_attested but the required attestation block is missing.
idempotency_key_reuse422The same Idempotency-Key was sent with a different request body.
sandbox_only403A sandbox-only action (e.g. minting a per-Principal sandbox key) was attempted with a live credential.
validation_error422Generic request-schema validation failure (missing/typed/out-of-range field).
unauthorized401Authentication failed — missing or invalid X-API-Key / X-Admin-Key.
forbidden403Authorization failed — scope, IP allowlist, cross-tenant access, or invalid admin key.
Branch on the code, not the message

Messages are tuned for humans and may change. The error code and HTTP status are the stable contract.

Idempotency

The onboarding create endpoints — POST /onboard, POST /principals, POST /principals/{id}/agents, and POST /agents/{id}/mandates — accept an Idempotency-Key header so a retried request can't create a duplicate.

Use a fresh UUID per logical operation — generate it once for the operation, reuse it across network retries of that same operation, and never recycle it for a different payload.

curl -X POST https://api.mandatelabs.ai/api/v1/principals \
  -H "X-API-Key: mdt_live_…" \
  -H "Idempotency-Key: 8f1c2e3a-9b7d-4f60-a1c2-2d3e4f5a6b7c" \
  -H "Content-Type: application/json" \
  -d '{
    "principal_kind": "merchant",
    "program_ids": ["prg_…"],
    "client_customer_ref": "acme-001",
    "profile": { "display_name": "Acme Corp", "email": "[email protected]", "country": "US" },
    "verification": {
      "type": "KYB", "mode": "client_attested",
      "attestation": { "attested_by": "[email protected]", "scope": "Tier-2 CDD",
        "statement": "Acme performed CDD on this merchant", "evidence_ref": "case-123" }
    }
  }'

# Re-sending the identical request with the SAME Idempotency-Key
# replays the original 201 response — no duplicate Principal is created.
Authorization idempotency

POST /authorize takes its idempotency key in the request body as idempotency_key (8–128 chars), not the header — a duplicate transaction with the same key replays the original decision instead of producing a second Authorization.

Retries & rate limits

A simple, safe policy:

Authorization is fail-safe

The /authorize endpoint is engineered never to answer 500 to a caller — on an internal fault it returns a safe decision rather than an error. You should still apply backoff to transient network failures.

Next steps