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.
{ "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" }}