Tab Docs

Errors

Every stable error code the public APIs return — status, meaning, and how to recover.

Every error is the same envelope, with cache-control: no-store:

{ "error": { "code": "PAYMENT_NOT_FOUND", "message": "Payment was not found." } }

Codes are stable — branch on error.code, never on message text. The SDK surfaces them as TabApiError.code / CheckoutApiError.code.

Authentication (all endpoints)

CodeStatusMeaningRecovery
INVALID_API_KEY401Key missing, malformed, revoked, or wrong type for the routeCheck the Authorization: Bearer header and key prefix (pk_/sk_)
API_KEY_PERMISSION_DENIED403Secret key lacks the needed permission (mutations need full)Use a full-access key for manage operations
API_KEY_ENVIRONMENT_DENIED403env query conflicts with the key's environmentKeys are per-environment; drop the param or switch keys

Payment intents — POST /v1/payment-intents

CodeStatusMeaningRecovery
INVALID_PAYMENT_INTENT_REQUEST400Body must be exactly { amount, intentUrl } with a valid USD amount and HTTPS urlFix the field noted in the message
PAYMENT_INTENT_SIGNING_UNAVAILABLE503Signing secret not configured server-sideTransient/config — retry or check deployment env

Payments — POST/GET /v1/payments, GET/PATCH /v1/payments/:id

CodeStatusMeaningRecovery
PAYMENT_REQUEST_TOO_LARGE413Create body over 10 KBSend only { intentToken }
INVALID_PAYMENT_INTENT_TOKEN400Token malformed, tampered, or wrong merchant/envMint a fresh intent
PAYMENT_INTENT_STALE409Intent expired (5-minute TTL)Mint a fresh intent
PAYMENT_INTENT_CONFLICT409Intent reused with different payment detailsEvery checkout needs its own intent
PAYMENT_CREATION_UNAVAILABLE503Database unavailableRetry with backoff
INVALID_PAYMENT_ID400Path id is not a UUID
PAYMENT_NOT_FOUND404Not this key's payment (tenant + env scoped)
INVALID_ENVIRONMENT / INVALID_LIMIT400Bad list query (limit 1–100)

Settlement reports — PATCH /v1/payments/:id

CodeStatusMeaningRecovery
PAYMENT_REPORT_TOO_LARGE413Report body over 120 KB
INVALID_PAYMENT_REPORT400Body must be exactly { buyerDidToken, transactionId, tokenChanges }; Testnet transactionId must be a 0x transaction hash
INVALID_DID_TOKEN401Buyer's Magic token failed verificationRe-authenticate the buyer
MAGIC_NOT_CONFIGURED503Magic secret not configured server-sideDeployment config
MAGIC_UNAVAILABLE502Magic verification transiently failedRetry
PAYMENT_REPORT_CONFLICT409Different evidence already reported for this paymentThe first report wins; never rewrite evidence
PAYMENT_VERIFICATION_FAILED422The reported Base Sepolia transaction doesn't verify: reverted, or no matching USDC Transfer payer → receiver at the intent amountThe transfer must actually exist — nothing was persisted
TEST_VERIFICATION_UNAVAILABLE503Base Sepolia RPC unreachableRetry — the SDK does this automatically

A not-yet-indexed transaction is not an error: the report returns 202 with verification.code: "TEST_SETTLEMENT_PENDING" and should be retried.

Checkout context & test rail

CodeStatusMeaning
CHECKOUT_CONFIGURATION_UNAVAILABLE503Client config incomplete server-side
LIVE_MODE_NO_TEST_BALANCE / LIVE_MODE_NO_TEST_FUNDS403Test rail is Testnet-only; live keys are refused
INVALID_TEST_BALANCE_REQUEST / INVALID_TEST_FUNDS_REQUEST400Bad address / missing buyerDidToken
TEST_BALANCE_UNAVAILABLE / TEST_FUNDS_UNAVAILABLE503RPC or faucet funder unavailable
TEST_FUNDS_RATE_LIMITED429Faucet limits hit (3/recipient/day, 25/merchant/day, 20/IP/hour). Honors Retry-After

Playground — /v1/checkout/playground-intent

CodeStatusMeaning
PLAYGROUND_TESTNET_ONLY403Live keys are refused; Mainnet unlocks after live verification
INVALID_PLAYGROUND_AMOUNT400Amount must be 0.50, 1.00, 2.00, or 5.00

Webhook management — /v1/webhook-endpoint

CodeStatusMeaning
INVALID_WEBHOOK_URL400Must be public HTTPS ≤2048 chars — loopback, private IPs, and raw IPs are rejected
WEBHOOK_ENDPOINT_EXISTS409One endpoint per environment; PATCH to change the URL
WEBHOOK_ENDPOINT_NOT_FOUND404No active endpoint in this environment
WEBHOOK_SECRET_UNAVAILABLE503Secret encryption key not configured server-side

Agent proxy — /api/agent/* (Bearer agent_sk_…)

The x402 auto-payer's own API. Every blocked or failed attempt still writes a receipt.

CodeStatusMeaning
INVALID_AGENT_KEY401Key unknown, revoked, or rotated
AGENT_PAUSED / AGENT_FROZEN / AGENT_CANCELLED423The owner's emergency ladder is engaged — payments refuse
CAP_EXCEEDED403This payment would exceed the per-cycle cap. A blocked receipt is written
CAP_NOT_SET403Owner hasn't set a cap yet
CAP_CYCLE_CHANGED409Cap cycle rolled mid-request — retry
FLOAT_EMPTY402No USDC at the signing address on the target network
FLOAT_CHECK_UNAVAILABLE503Balance read failed
AUTHORIZATION_EXPIRED409The x402 authorization window lapsed
SIGN_RATE_LIMITED429Signing rate limit (honors Retry-After)
SIGN_REQUEST_IN_PROGRESS / SIGN_REQUEST_RECONCILING / SIGN_REQUEST_CONFLICT409Durable-envelope concurrency control — retry per Retry-After
SIGNER_NOT_CONFIGURED503Wallet-provider (Magic TEE) configuration incomplete
SIGNER_PROVIDER_REJECTED / SIGNER_PROVIDER_INVALID_RESPONSE / SIGNER_IDENTITY_MISMATCH502The provider refused or answered out of contract — the message includes the provider stage
SIGNER_PROVIDER_UNAVAILABLE / SIGNER_PROVIDER_TIMEOUT503Provider transiently down — retry
RECEIPT_NOT_FOUND / RECEIPT_NOT_PENDING / SETTLEMENT_RESULT_CONFLICT404/409Result reporting against a missing or already-final receipt

Everything else

Dashboard, auth, faucet, and internal cron routes use the same envelope with their own codes (SESSION_REQUIRED, ORIGIN_NOT_ALLOWED, FAUCET_RATE_LIMITED, …). They are session-authenticated and not part of the public API surface.

On this page