> For the complete index of the 402pay docs, see [llms.txt](https://developer.402pay.co/llms.txt).

# Errors

Status codes, the error envelope, and the codes to branch on.

Errors use standard HTTP status codes and one envelope. `code` is stable, so branch on it. `message` is written for people and may change. `field` names the input at fault, when there is one, and `request_id` matches the `402pay-Request-Id` header.

## Status codes

| Status | Meaning |
| --- | --- |
| 400 | The request is malformed or a field is invalid. |
| 401 | There's no valid key. The response has a WWW-Authenticate: Bearer header. |
| 403 | The key can't do this, or the request came from the wrong place. |
| 404 | Nothing with that ID belongs to your business, or the path doesn't exist. |
| 405 | The path exists but doesn't take this method. The response is empty, with no request ID. |
| 409 | The request conflicts with the object's current state. |
| 410 | What's being paid has expired. |
| 415 | The body isn't JSON. |
| 429 | Too many requests or attempts. |
| 500 | Something failed on our side. |
| 503 | This deployment can't do it right now, such as sending an email or reaching a network. |

## Error codes

### Requests and keys

| Code | Status | When |
| --- | --- | --- |
| `invalid_request` | 400 | A field is missing, unknown or invalid. `field` names it. Metadata refuses reserved names with "Metadata keys can't be named {key}." On `POST /wallet`, reused keys answer "These keys are already connected to another business." Text fields can't contain null characters ("{field} must not contain null characters.") or an escaped unpaired surrogate such as `\ud800` ("{field} must be valid text."). |
| `invalid_json` | 400 | The body isn't valid JSON, or isn't UTF-8. |
| `idempotency_key_required` | 400 | `POST /checkouts`, `POST /payments/{id}/request-remainder` and `POST /wallet/transactions` need an `Idempotency-Key` header. |
| `unsupported_media_type` | 415 | A body was sent without `Content-Type: application/json`. |
| `payload_too_large` | 413 | The request body is larger than 1 MiB. |
| `unauthenticated` | 401 | No key was sent. |
| `invalid_api_key` | 401 | The key is invalid or revoked, it's a publishable key, or the `Authorization` header isn't `Bearer` followed by the key. |
| `business_forbidden` | 403 | The `402pay-Business` header names a business the key or the signed-in person can't act for. With a secret key, leave the header out. |
| `permission_denied` | 403 | A restricted key doesn't have the access this route needs. |
| `test_mode_unavailable` | 403 | On a live business, whose payments are real money, a test key tried to create a payment, open a checkout or request a remainder, or to change what customers see or pay: links, checkout settings, accepting or canceling a payment, or blocking a customer. |
| `session_required` | 403 | The route is for the dashboard only, such as managing API keys or sending from your wallet, so an API key can't use it. |
| `cross_site_request` | 403 | A request signed in with a cookie came from another site. |
| `edge_forbidden` | 403 | The request didn't come through `https://dash.402pay.co/api/v1`, the API's only address. |
| `not_found` | 404 | Nothing with that ID belongs to your business, or the path doesn't exist. |
| `idempotency_key_reused` | 409 | With a secret key or a session, the key was already used for a different request. |
| `idempotency_key_in_use` | 409 | The first request with this key is still running. Try again in a moment. |
| `rate_limited` | 429 | Too many requests from one IP address, or more than 10 webhook test events and resends in a minute for one business. Wait for the `Retry-After` seconds. |
| `internal_error` | 500 | Something failed on our side. Retry with the same Idempotency-Key. |
| `temporarily_unavailable` | 503 | 402pay paused something for now, such as new checkouts, card payments, a network or new sign-ups. Try again later. |
| `upstream_unavailable` | 503 | 402pay can't be reached right now. Retry with the same Idempotency-Key. |

### Payments

| Code | Status | When |
| --- | --- | --- |
| `invalid_email` | 400 | `customer_email` on a payment, or `email` on a checkout, isn't a valid address. Other routes, such as customers, answer `invalid_request` with `field` set instead. |
| `reference_in_use` | 409 | The reference names a live payment for another amount or currency, or one that already received funds. |
| `not_accepting` | 409 | The business has no wallet yet, or its wallet can't receive on the network a remainder needs. |
| `business_suspended` | 403 | 402pay suspended the business, so it can't create payments or open checkouts. Checkouts already open can still finish. |
| `payment_not_cancelable` | 409 | Funds have arrived, or the payment came from a link. |
| `payment_in_flight` | 409 | A transfer for the payment is on its way, so it can't be canceled. |
| `payment_not_acceptable` | 409 | Only `underpaid` and `needs_review` payments can be accepted. |
| `payment_not_underpaid` | 409 | Only an `underpaid` payment has a remainder to request. |
| `remainder_unavailable` | 409 | The quote is still live and the customer can still send the rest, or the payment has no checkout page left. |
| `nothing_due` | 409 | Nothing is left to collect on the payment. |

### Checkouts and receipts

| Code | Status | When |
| --- | --- | --- |
| `email_required` | 400 | The rail or the business needs the customer's email, and none was sent. |
| `rail_unavailable` | 400 | The business doesn't accept this rail. |
| `asset_unavailable` | 400 | The business doesn't accept this coin on this network. |
| `amount_out_of_range` | 400 | The price is outside what cards allow, $5 to $10,000. |
| `invalid_tx_hash` | 400 | A claim's transaction hash doesn't fit the checkout's network. |
| `customer_blocked` | 403 | The business has blocked the customer with this email. |
| `link_not_found` | 404 | No active link has this code. |
| `payment_not_found` | 404 | No payment has this code. |
| `payment_paid` | 409 | Funds already reached the payment: it's `succeeded`, `underpaid` or `needs_review`, so its page can't start another attempt. |
| `payment_canceled` | 409 | The business canceled the payment. |
| `wrong_rail` | 409 | The action is for crypto checkouts, and this one is paid by card. |
| `checkout_canceled` | 409 | The checkout was canceled. |
| `checkout_failed` | 409 | The checkout failed. Start a new one. |
| `checkout_not_cancelable` | 409 | Funds may already be on the way. |
| `checkout_not_supersedable` | 409 | The checkout can't be replaced now, because funds may be on the way or it collects a remainder. |
| `checkout_not_claimable` | 409 | Only an expired or canceled checkout takes a claim. |
| `already_claimed` | 409 | The business is already reviewing a claim for this checkout. |
| `checkout_not_retryable` | 409 | The card checkout can't start another attempt now. |
| `route_in_progress` | 409 | The customer's payment on the current secure payment page may still go through, so the checkout can't start another attempt until it finishes. |
| `no_route` | 409 | No further card-payment attempt is available. |
| `stale_route` | 409 | This card payment page is out of date. |
| `checkout_not_awaiting` | 409 | The checkout is not waiting for a card payment. |
| `payment_expired` | 410 | The payment passed its `expires_at`. |
| `checkout_expired` | 410 | The checkout's quote expired. Start a new one for a fresh address and amount. |
| `link_archived` | 410 | The link was archived, so its page takes no new payments. |

### Links and customers

| Code | Status | When |
| --- | --- | --- |
| `link_has_payments` | 409 | A link with payments can't be deleted. Archive it instead. |
| `link_disabled` | 409 | 402pay disabled the link, so it can't be restored. |
| `customer_exists` | 409 | Another customer already has this email. |

### Dashboard and account

These come from routes only the dashboard and its sign-up and sign-in pages use.

| Code | Status | When |
| --- | --- | --- |
| `business_required` | 400 | The request didn't name a business in the 402pay-Business header. |
| `invalid_credentials` | 400 | The email and password don't match an account, or the current password is wrong. An unknown email, or an account without a password, gets the same answer as a wrong password. |
| `challenge_failed` | 400 | Sign-up, sign-in or a password reset came without a passed Cloudflare Turnstile check in the `402pay-Challenge` header, or with one Cloudflare refused, one already used or one from another form. Complete the check again and resend. |
| `weak_password` | 400 | The new password breaks a rule: too short or long, too repetitive, without letters, too common, or found in a data breach. The message says which. |
| `password_required` | 400 | The sign-up is being finished in a different browser, so the password chosen at sign-up is needed too. |
| `signup_browser_required` | 400 | A sign-up with a passkey is being finished in a different browser. It has no password to prove who started it, so enter the code in the browser that did. |
| `invalid_code` | 400 | The code from the email, the authenticator code or the recovery code is wrong or malformed. |
| `code_expired` | 400 | The emailed code is more than 15 minutes old. Send a new one. |
| `verification_expired` | 400 or 409 | The sign-up, email change or new alert email was finished, canceled or replaced, or is more than a day old. Start again. |
| `invalid_token` | 400 | The password reset, undo or wallet change link was already used, replaced by a newer one or expired. |
| `challenge_expired` | 400 | Two-step sign-in timed out or was already used. Sign in again. |
| `passkey_failed` | 400 | The passkey couldn't be verified: its challenge is unknown, expired, already used or another browser's, or its signature, site, user verification or sign counter doesn't check out. Start again for a new challenge. |
| `passkey_unknown` | 400 | The passkey isn't on any account, because it was removed or never added, or at a step-up it isn't one of this account's. |
| `password_reset_required` | 403 | The password stopped working after too many failed sign-ins, or after an email change was undone. Reset it to sign in. The message says which. |
| `reset_on_hold` | 409 | An email change was undone, so nobody can choose a new password until the hold ends, a day later. `Retry-After` says when. |
| `step_up_required` | 403 | The change decides where payments go or who can reach the business, and the session hasn't proven its password or a passkey in the last 15 minutes. Confirm it at `POST /sessions/current/step-up` and send the request again. API keys are never asked. |
| `too_many_attempts` | 429 | Too many wrong codes or passwords: five wrong two-step codes lock the person's codes for 15 minutes, five wrong tries use up an emailed code, ten wrong sign-up codes in a day lock an address's sign-ups, and failed passwords lock the email (or the session, for a current password) for a while. Wait for the `Retry-After` seconds; signing in again doesn't reset a lock. |
| `too_many_emails` | 429 | We've sent this address as many emails as we can for now, a new code was asked for too soon, or the person started too many email changes or new alert emails in the last hour. `Retry-After` says when to try again. |
| `email_unavailable` | 503 | This deployment can't send email, so sign-up, password resets, email changes and new alert emails aren't available on it. |
| `email_taken` | 409 | Another account already uses this email address. |
| `two_factor_setup_required` | 409 | The two-step setup ended: it was never started in this session, or its QR code is more than 10 minutes old. Start again for a new one. |
| `two_factor_enabled` | 409 | Two-step verification is already on. |
| `two_factor_disabled` | 409 | Two-step verification is off. |
| `passkey_exists` | 409 | The passkey being added is already on an account. |
| `passkey_limit` | 409 | The account already has 10 passkeys. Remove one to add another. |
| `last_sign_in_method` | 409 | Removing this passkey would leave the account no way to sign in. Add another passkey or set a password first. |
| `no_passkeys` | 409 | A passkey step-up was asked for on an account without passkeys. Confirm with the password instead. |
| `current_session` | 409 | Sign out to end the session you're using. |
| `publishable_key` | 409 | A publishable key has no name or permissions to change. |
| `api_key_revoked` | 409 | A revoked key can't be changed. |
| `wallet_exists` | 409 | The business already has a wallet. |
| `wallet_required` | 409 | The business needs a wallet before it can finish setup. |
| `referral_locked` | 409 | The business already has a wallet, so the referral code it signed up with can't change. |
| `external_wallet` | 409 | The wallet keeps its own keys, so there's no recovery phrase here. |
| `wallet_change_pending` | 409 | The wallet is already set to be deleted. Cancel the deletion first, or wait for it. |
| `wallet_change_applied` | 409 | The wallet change already took effect, so it can't be canceled. |
| `invalid_address` | 400 | A send's address isn't valid for the network, or belongs to this wallet. |
| `insufficient_funds` | 409 | A send is for more than the wallet holds. |
| `insufficient_fee_funds` | 409 | A token send needs the network's own coin for its fee, and the wallet doesn't hold enough of it. |
| `duplicate_transaction` | 409 | That transaction was already sent, so it's on the wallet's history. |
| `transaction_rejected` | 409 | The network refused a send, such as when another transaction from the address got there first or the fee was too low. Nothing was sent, so read a fresh send context and sign again. |
| `network_unavailable` | 503 | The network a send or its context needs couldn't be reached. Retry with the same `Idempotency-Key`: the same signed transaction never goes out twice. |

## Rate limits

Some routes limit how often one IP address can call them, and answer 429 `rate_limited` with a `Retry-After` header, in seconds, once it's used up.

| Route | Limit per IP address |
| --- | --- |
| `POST /checkouts` | 30 per 10 minutes. |
| `POST /accounts` | 10 per hour. |
| `POST /accounts/verify` and `/accounts/verify/resend` | 20 per 10 minutes, together. |
| `POST /sessions` | 30 per 10 minutes. |
| `POST /sessions/two-factor` | 30 per 10 minutes. |
| `POST /passkeys/authentication/options` and `/passkeys/authentication/verify` | 60 per 10 minutes, together. |
| `POST /me/password`, `/me/two-factor`, `/sessions/current/step-up` and `/sessions/current/step-up/options` | 20 per 10 minutes, together. |
| `POST /me/passkeys/registration/options` and `POST /me/passkeys` | 20 per 10 minutes, together. |
| `POST /me/email` | 10 per hour. |
| `POST /password-resets` | 5 per hour. |
| `POST /password-resets/check`, `/password-resets/confirm`, `POST /email-changes/undo` and `POST /wallet-changes/cancel` | 20 per 10 minutes, together. |
| `GET /public/referrals/{code}` | 60 per 10 minutes. |

Sign-in and email also have limits that count per person, email address or code rather than per IP address, so they apply wherever the request comes from, and starting a new sign-in doesn't reset them:

| What | Limit |
| --- | --- |
| Failed passwords, per email | 5 in a row lock it for 1 minute, 10 for 5 minutes, 15 for 15 minutes, and 20 and every 5 after for an hour, with 429 `too_many_attempts`. After 100, the password stops working until it's reset: 403 `password_reset_required`. A success or a reset clears the count. A browser that signed in to the account before keeps a count of its own, which locks the same way but never turns the password off, so failures from elsewhere can't lock its owner out. |
| Wrong current passwords, per session | The same locks for `current_password` on `/me/password`, `/me/email`, `/me/two-factor` and `/sessions/current/step-up`, counted per session. |
| Two-step codes, per person | 5 wrong codes, counted across every challenge, lock that person's codes for 15 minutes. Every code check then answers 429 `too_many_attempts`. |
| Emailed codes, per code | 5 wrong tries, then 429 `too_many_attempts` until a new code is sent. A new code can be sent a minute after the last, up to 5 per sign-up, email change or new alert email. |
| Emails, per address | 5 an hour and 20 a day from sign-ups, password resets, email changes and new alert emails, and at most 3 reset links an hour. Past that, a resend or a new alert email answers 429 `too_many_emails`, and other requests answer as usual but send nothing. |
| Sign-up codes, per address | 10 wrong codes a day, across every sign-up for the address, then 429 `too_many_attempts`. |
| Email changes, per person | 3 started an hour, whether or not the address was free, then 429 `too_many_emails`. |
| New alert emails, per person | 3 started an hour across all their businesses, including ones since replaced or canceled, then 429 `too_many_emails`. Resending a code and addresses that apply at once don't count. |

Each of these 429 answers carries a `Retry-After` header, in seconds.

Response, 400 Bad Request:

```json
{
  "error": {
    "code": "invalid_request",
    "message": "amount must be a whole number of minor units between $1.00 and $1,000,000.00.",
    "field": "amount",
    "request_id": "req_6eb3d90a19234e358f603a74e004c874"
  }
}
```
