Error code reference
Every non-2xx response uses the canonical error envelope;
branch on code, never on message. This page covers the codes introduced
by the identity/credential, authority/approvals, unified-payments, and
webhooks surfaces. Statuses shown are the code's canonical status.
Verification sessions & handoff
code | Status | Meaning |
|---|---|---|
VERIFICATION_SESSION_NOT_FOUND | 404 | No session with that id in your tenant. |
VERIFICATION_SESSION_TERMINAL | 409 | Session already reached a terminal state (VERIFIED/REJECTED/EXPIRED/CANCELED/ERROR). |
VERIFICATION_SESSION_NOT_STARTED | 409 | No provider session exists to advance. |
SIMULATE_NOT_AVAILABLE | 403 | /simulate needs a TEST key + the mock provider — LIVE sessions are never advanceable by API call. |
MEMBER_ID_REQUIRED | 400 | KYC sessions need memberId. |
MEMBER_NOT_FOUND | 404 | No such member in your tenant. |
MEMBER_NOT_ACTIVE | 409 | Only ACTIVE members can be verified / hold authority. |
LEGAL_NAME_REQUIRED | 400 | No legal name available for the subject. |
HANDOFF_GONE | 410 | The one-time QR link was already consumed or expired — mint a new one. |
Credentials
code | Status | Meaning |
|---|---|---|
CREDENTIAL_NOT_FOUND | 404 | No credential with that id in your tenant. |
CREDENTIAL_NOT_REFRESHABLE | 404 | The credential has no provider linkage to re-read. |
CREDENTIAL_ARTIFACT_NOT_AVAILABLE | 404 | Legacy unsigned row — re-issuance supersedes it; there is no artifact to fetch. |
UNKNOWN_ELIGIBILITY_ACTION | 400 | The eligibility endpoint only evaluates known gate actions. |
Identities & wallet bindings
code | Status | Meaning |
|---|---|---|
IDENTITY_NOT_FOUND | 404 | No identity with that id (or DID) in your tenant. |
TENANT_NOT_FOUND | 404 | TENANT-subject identity requests are fixed to your own tenant. |
INVALID_WALLET_ADDRESS | 400 | Not a valid EVM address. |
WALLET_BINDING_NOT_FOUND | 404 | No such binding under that identity. |
WALLET_BINDING_NOT_PENDING | 409 | Binding is not PENDING (already verified, revoked, or concurrently consumed). |
WALLET_BINDING_NO_CHALLENGE | 409 | Binding has no outstanding challenge. |
WALLET_BINDING_SIGNATURE_INVALID | 400 | Signature does not recover to the wallet being bound (sign the exact challenge text with EIP-191 personal_sign). |
WALLET_BINDING_NOT_ACTIVE | 409 | Only ACTIVE bindings can be rotated. |
WALLET_BINDING_NOT_ROTATING | 409 | The binding is not an open rotation. |
WALLET_BINDING_ROTATION_STALE | 409 | The binding being replaced stopped being ACTIVE mid-rotation — start over. |
Credential presentations
code | Status | Meaning |
|---|---|---|
CHALLENGE_NOT_FOUND | 404 | Unknown nonce (or another tenant's challenge). |
PRESENTATION_REPLAYED | 409 | The challenge was already consumed — presentations are single-use, and a failed verify burns its challenge too. |
Verification failures are NOT errors: POST …/verify returns 200 with
verified: false and reasonCodes (CHALLENGE_EXPIRED, HOLDER_UNKNOWN,
HOLDER_MISMATCH, SIGNER_NOT_BOUND, PRESENTATION_SIGNATURE_INVALID,
CREDENTIAL_INVALID) plus per-credential verdicts.
Authority & payment policy
code | Status | Meaning |
|---|---|---|
AUTHORITY_NOT_FOUND | 404 | No authority with that id in your tenant. |
AUTHORITY_ACTIONS_REQUIRED | 400 | allowedActions must name at least one gate action. |
INVALID_USD_AMOUNT | 400 | Limits must be positive USD amounts (≤ 6 fraction digits). |
INVALID_APPROVAL_WEIGHT | 400 | approvalWeight must be ≥ 1. |
AUTHORITY_REVOKED | 409 | A revoked authority is immutable — grant a new one. |
AUTHORITY_NOT_ACTIVE | 409 | Only ACTIVE authorities can be attested. |
AUTHORITY_ALREADY_ATTESTED | 409 | The authority already carries its credential. |
ORG_SAFE_NOT_DEPLOYED | 409 | Org-signed credentials require a DEPLOYED tenant Safe (EIP-1271); no fallback signer exists. |
ORG_ATTESTATION_INVALID | 400 | The Safe signature does not validate on-chain (threshold not met or wrong hash). |
PAYMENT_POLICY_NOT_FOUND | 404 | No policy version to attest. |
PAYMENT_POLICY_ALREADY_ATTESTED | 400 | The active policy version is already attested. |
PAYMENT_POLICY_UNKNOWN_KEY | 400 | Unknown rule key in the policy document. |
INVALID_POLICY_RULE | 400 | A rule value failed validation. |
Approvals
code | Status | Meaning |
|---|---|---|
APPROVAL_NOT_FOUND | 404 | No approval request with that id in your tenant. |
APPROVAL_NOT_PENDING | 409 (400 on /payments/{id}/approve when none exists) | The request is not open for votes / the payment has no pending request. |
APPROVAL_REQUIRES_MEMBER | 403 | Votes come from members, not applications — an API key cannot approve. |
SELF_APPROVAL_FORBIDDEN | 403 | The member who initiated a payment can never approve it (segregation of duties). |
AUTHORITY_MISSING | 403 | No ACTIVE authority grants payment.approve (also a gate hold reason on payments). |
APPROVAL_GROUP_MISMATCH | 403 | The request is scoped to a different approval group. |
ALREADY_VOTED | 409 | One vote per member per request. |
APPROVAL_SIGNATURE_INVALID | 400 | The optional vote signature does not recover to the member's signer address. |
NO_SIGNER_ADDRESS | 400 | The member has no registered signer address to verify a signature against. |
Unified payments (/v1/payments)
code | Status | Meaning |
|---|---|---|
AMBIGUOUS_PAYMENT_BODY | 400 | Body carries both recipient and sourceWalletId — send one generation, not both. |
UNRECOGNIZED_PAYMENT_BODY | 400 | Neither recipient (intent) nor sourceWalletId (legacy) present. |
INVALID_PAYMENT_BODY | 400 | The normalized intent failed validation (details lists issues); it never falls through to the legacy path. |
RECIPIENT_REQUIRED | 400 | An intent needs a resolvable recipient. |
RECIPIENT_AMBIGUOUS | 400 | The recipient object matched more than one resolution key — send exactly one of did / phoneNumber / email / identityId. |
RECIPIENT_NOT_ROUTABLE | 400 | The recipient exists but cannot receive routed payments. |
PAYEE_NOT_FOUND / EMAIL_NOT_FOUND / IDENTITY_NOT_FOUND | 404 | The recipient reference did not resolve. A Permara ID nobody holds is never reserved by guessing — invite by email or mobile number instead. |
INVALID_DID | 400 | Not a did:permara: identifier (64 lowercase hex characters after the prefix). |
DID_NOT_RESOLVABLE | 400 | A DID of another method; only did:permara resolves here. |
INVITE_CONTACT_REQUIRED | 400 | Reserving an identity needs an email or a phone. |
UNSUPPORTED_RECIPIENT_KIND | 400 | The recipient kind is not supported on the intent path. |
INVALID_AMOUNT | 400 | Not a valid USD amount. |
INVALID_PHONE | 400 | Not a valid E.164 phone number. |
FUNDING_MODE_NOT_AVAILABLE | 400 | The requested funding mode is not available for the tenant. |
DESTINATION_NOT_ELIGIBLE | 403 | Client-supplied destination is not owned + ACTIVE for the resolved recipient. |
PAYMENT_NOT_FOUND | 404 | Unknown payment id (across both generations). |
NOT_SENDER | 403 | Only the sending tenant may perform this action. |
RECIPIENT_CONSENT_REQUIRED | 403 | Route selection/confirmation belongs to the recipient. |
ROUTE_NOT_SELECTED | 409 | Confirm requires a selected route quote. |
ROUTE_QUOTE_EXPIRED | 409 | The selected quote expired — re-fetch /routes and re-select. |
ROUTE_QUOTE_NOT_AVAILABLE | 404 | The quote no longer exists. |
RECIPIENT_UNRESOLVED | 409 | Routing needs a resolved recipient (still in claim/onboarding). |
CANCEL_CONFLICT | 409 | The payment is past the point of cancellation. |
INVALID_TRANSITION | 400 | The requested action is illegal from the payment's current state. |
IDEMPOTENCY_KEY_REUSED | 409 | The idempotency key was reused with a different payment body (salient-field hash mismatch). The global interceptor also rejects raw body-hash mismatches with 422. |
LIQUIDITY_INSUFFICIENT / LIQUIDITY_NOT_CONFIGURED / LIQUIDITY_BAD_AMOUNT | 409 / 404 / 409 | Provider prefunded-liquidity constraints. |
REVIEW_CASE_NOT_FOUND / REVIEW_CASE_NOT_OPEN / REVIEW_CASE_RESOLVED | 404 / 409 / 409 | Admin review-case lifecycle. |
Balance & funding (/v1/balance, /v1/faucet/drips)
| Code | HTTP | Meaning |
|---|---|---|
INSUFFICIENT_FUNDS | 402 | The payer's primary account holds less than the payment needs. details.shortfallBaseUnits says how much; details.funding says how money gets in (FAUCET on a testnet). |
ACCOUNT_NOT_READY | 409 | No account holds the payer's money yet (details.status). GET /v1/balance?ensure=true provisions it. |
ACCOUNT_CHAIN_MISMATCH | 400 | The payment names a chain the payer's account is not on. |
BALANCE_UNAVAILABLE | 503 | The balance could not be read; nothing is assumed available. Retry. |
SMART_ACCOUNT_NOT_LINKED | 409 | The member has no smart account yet. Sign in to the app once (Privy links it), or GET /v1/balance?ensure=true. |
ORG_ACCOUNT_NOT_READY | 409 | The organization account is missing or not deployed yet; details.status says which. GET /v1/balance?ensure=true provisions it. |
FAUCET_NOT_AVAILABLE | 403 | Demo USD is minted on testnets only; the account's chain is a mainnet. |
FAUCET_RATE_LIMITED | 403 | The funded address (20/day) or the tenant (100/day) has used its drips. |
AMOUNT_EXCEEDS_CAP | 400 | More than 1000 Demo USD (or 0.02 ETH) in one drip. |
Payout destinations
code | Status | Meaning |
|---|---|---|
DESTINATION_NOT_FOUND | 404 | No destination with that id. |
DESTINATION_EXISTS | 409 | An equivalent destination already exists. |
DESTINATION_REVOKED | 409 | Revoked destinations never mutate back — create a new one. |
DESTINATION_NOT_VERIFIED / DESTINATION_NOT_ACTIVE | 409 | Status is enforced at the read path — the router only accepts ACTIVE. |
DESTINATION_VERIFY_NOT_AVAILABLE | 403 | No verification flow available for this rail. |
DESTINATION_TYPE_REQUIRED / DESTINATION_IDENTIFIER_REQUIRED / DESTINATION_RAIL_CONSTRAINT / DESTINATION_RAIL_NOT_AVAILABLE | 400 | Rail-specific input validation. |
UNSUPPORTED_NETWORK / INVALID_DESTINATION_ADDRESS | 400 | On-chain destination validation. |
IDENTITY_REQUIRED | 403 | The identity gate blocked the action (details.missing lists credential kinds). |
Webhooks (outbound)
code | Status | Meaning |
|---|---|---|
INVALID_URL | 400 | Not a valid absolute URL. |
SCHEME_NOT_ALLOWED | 400 | LIVE endpoints must be https; TEST may use http. |
PRIVATE_HOST_BLOCKED | 400 | The host is not a public address (loopback/RFC1918/link-local/metadata/CGNAT are refused — SSRF guard, not overridable). |
PORT_NOT_ALLOWED | 400 | LIVE endpoints must target port 443. |
ENDPOINT_NOT_FOUND | 404 | No endpoint with that id in your tenant. |
DELIVERY_NOT_FOUND | 404 | Unknown delivery id. |
Delivery attempts fail with a coarse lastError enum on the delivery row
(TIMEOUT, CONNECTION_FAILED, BLOCKED_URL, HTTP_4XX, HTTP_5XX,
HTTP_ERROR, SECRET_ERROR, LEASE_EXPIRED, ENDPOINT_GONE) — see
Webhooks. Inbound provider webhooks reject with
WEBHOOK_SIGNATURE_INVALID (401), WEBHOOK_REPLAY_WINDOW (401),
EMPTY_BODY (400), UNKNOWN_PROVIDER (400).
API-key hardening
code | Status | Meaning |
|---|---|---|
API_KEY_REQUIRED / API_KEY_INVALID / API_KEY_REVOKED / API_KEY_EXPIRED | 401 | Key missing, malformed/unknown/environment-mismatched, revoked, or expired. |
AGENT_SUSPENDED | 401 | The agent behind this key is suspended (checked on every request, cache included). |
IP_NOT_ALLOWED | 403 | The key has an IP allowlist and the request's source IP is not on it. |
REQUEST_SIGNATURE_REQUIRED | 403 | This key requires signed requests on high-risk routes (x-safebank-request-signature: t=<unix>,v1=<hmac>). |
REQUEST_SIGNATURE_STALE | 403 | Signature timestamp outside the 300 s tolerance. |
REQUEST_SIGNATURE_INVALID | 403 | HMAC over timestamp.METHOD.path.sha256(rawBody) does not verify. |
REQUEST_SIGNING_NOT_CONFIGURED | 403 | Signing is required but no signing secret is set for the key. |