On this page
Error Codes
Handle Merchant API failures with stable machine-readable codes, field-level validation details, and documented recovery paths.
Code catalogue
Stable machine-readable identifiers returned in code (and per-field in errors[].code). This is the live set — every code below is produced by at least one handler, validator, or middleware in the running API, not a theoretical superset. Existing codes never change semantics, and new codes are added as new scenarios arise. A code is renamed only in rare cases, and every rename is announced in the changelog.
Switch on code for programmatic handling. detail is English-only and intended for logs / fallback UI; localize on the client by code.
401 and 403 are not two grades of the same failure. A 401 means the request never got past the credential check: the header is absent, malformed, signed with the wrong secret, or signed too long ago. Sending it again unchanged returns the same answer. The fix is on the credential side — the key, its configuration in the dashboard, or the clock you sign with.
A 403 means the credential was accepted and something further along refused. Two unrelated situations share that status:
- Wrong key for this call —
forbidden,payment_key_required,payout_key_required. Integration configuration: sign with a key of the right type and environment, and the call goes through. - Right key, refused by account or project state —
whitelist_required,merchant_suspended,widget_inactive,terminal_not_enabled,embed_not_enabled,not_sandbox,payment_channel_simulation_sandbox_only. The credential is healthy and replacing it changes nothing. The common case iswhitelist_required: the destination address hasn't been added to the withdrawal whitelist yet. Show that to whoever is waiting on the payout, rather than paging an engineer to rotate a key that was never broken.
The HTTP status only says which layer refused. What to do about it is in code — not in the status, and not in whichever error class your SDK mapped it to.
Field-level codes
Emitted by request validation. Each code below sets field to the offending field name in snake_case. When several fields fail at once, the top-level code is validation_failed and every per-field failure lands in errors[].
| Code | When | What to do |
|---|---|---|
validation_failed |
The top-level code when several fields fail at once. Each per-field failure carries its own code and field in errors[]. |
Inspect errors[] and fix each listed field. |
field_required |
A required field is empty, null, or whitespace-only. | Provide a value for field. |
field_too_long |
Field exceeds the maximum allowed length. | Truncate to the limit documented for that endpoint. |
field_out_of_range |
Numeric value outside the allowed range. | Clamp the value to the documented range. |
field_invalid_enum |
Value is not one of the accepted enum members (e.g. unknown currency / network, or a token that isn't enabled for the project). |
Check the enum reference; for tokens, ensure the project has the token+network enabled. |
field_invalid_format |
Value doesn't match the shape the field expects — an unparseable id (e.g. invoice_id without the inv_… prefix), or an amount that isn't a plain decimal string such as "10.00". |
Send the field in its documented format; use ids returned by the API verbatim. |
entity_id_empty |
A supplied id is an all-zero / empty GUID (00000000-0000-…). field names the offending id — e.g. invoice_id or withdrawal_id. |
Send the id returned by the API verbatim; never a zeroed or empty id. |
field_must_be_positive |
Decimal/integer must be greater than 0. | Send a positive value. |
field_percentage_out_of_range |
Percentage must be between 0 and 100 inclusive. | Clamp the value to [0, 100]. |
query_parameter_unknown |
A list request contains a query parameter that the endpoint doesn't support. | Remove the parameter or use its documented snake_case name. |
pagination_cursor_invalid |
A cursor is malformed, expired, tampered with, belongs to another resource, or is combined with different filters. | Restart pagination without cursor, then keep the same filters while following next_cursor. |
amount_too_many_decimals |
For fiat-flow invoices, amount has more decimal places than the currency's minor unit (e.g. 23.345 for USD, 23.01 for JPY). |
Round to the currency's minor unit. |
amount_too_large |
amount on an invoice or withdrawal create is outside the supported range — above the 20-integer-digit ceiling the API stores. |
Send an amount within the supported range. |
Generic HTTP-status fallbacks
Returned when the failure has no specific business code attached. Each of these is a fallback for an HTTP status: the request failed, but no finer-grained code described the cause.
| Code | HTTP | When | What to do |
|---|---|---|---|
forbidden |
403 | Authenticated, but this credential isn't allowed to perform the operation — its scope (capability) or environment doesn't permit it, for example a Sandbox key calling a Production-only endpoint. | Call with a credential whose capability and environment match the operation. Ownership is never a 403: a resource that belongs to another merchant returns not_found (404), not forbidden. |
payment_key_required |
403 | The endpoint needs a Payment (pk_) key, but the request used a Payout (rk_) key — for example, listing invoices. |
Call it with a Payment key. |
payout_key_required |
403 | The endpoint needs a Payout (rk_) key, but the request used a Payment (pk_) key. Every withdrawal route answers this way, and so does reading balances. |
Call it with a Payout key. Note the Payout key also requires an IP whitelist before it works. |
internal_error |
500 | An unexpected error or bug on our side. | Retry once. If it keeps failing, contact support and include the time of the response. |
validation_failed |
400 | The request failed validation, but no single field-level code applied. | Read the detail field — it says what was rejected. |
Authentication
Returned only on 401 Unauthorized responses from the HMAC authentication handler. Each code identifies a specific failure point so clients can show an actionable message instead of a generic "auth failed".
| Code | HTTP | When | What to do |
|---|---|---|---|
unauthorized |
401 | Generic fallback when none of the specific codes below applied — typically a request to an auth-required endpoint with no Authorization header (or an unrecognized auth scheme). |
Send a valid HMAC Authorization header; re-check the signing flow end-to-end. |
authorization_missing |
401 | Authorization header value is empty (sent but blank). |
Send the full Authorization: HMAC-SHA256 {apiKeyId}:{base64signature} header. |
authorization_malformed |
401 | Header is present but malformed: scheme parses but the credentials part is missing the colon, the API key id is unparseable, or the signature is empty. | Re-build the header per Authentication; a typo in the API key id is the most common cause. |
timestamp_missing |
401 | X-Request-Timestamp header is missing or doesn't parse as a positive integer. |
Set the header to the current Unix timestamp in seconds. |
timestamp_expired |
401 | Timestamp is outside the allowed clock-skew window (default 5 minutes). | Sync your clock to NTP; re-sign with a fresh timestamp. |
invalid_credentials |
401 | The credential is unusable: unknown key id, revoked key, an environment/type prefix that doesn't match the stored key, or a signature that doesn't verify. Deliberately one code for all four — a distinguishable answer would confirm which key ids exist to a caller who holds no secret. | Check the API key id is correct and active, and that the signature is computed with the matching secret. Signing is the most common cause: verify your string-to-sign assembly ({ts}\n{METHOD}\n{path}\n{query}\n{bodyHash}) and that bodyHash is empty for empty bodies (don't hash an empty string). |
ip_not_allowed |
401 | The IP whitelist on a Payout key is configured and your caller IP is not in it. Only Payout keys carry an IP check — Payment keys are callable from any IP. Verified after the signature passes, so a valid signature from the wrong network still fails. | Add the caller IP (or its CIDR block) to the Payout key's whitelist in the dashboard. |
payout_whitelist_required |
401 | A newly generated Payout key has no IP whitelist configured yet. Payout keys stay inactive until the merchant adds at least one allowed IP — distinct from ip_not_allowed so clients can tell "dashboard config missing" apart from "wrong source IP". |
Open the dashboard, go to the API keys page, and configure at least one IP or CIDR in the Payout key's IP whitelist. |
Rate limit
Returned on 429 Too Many Requests when a request exceeds the per-merchant rate limit. The count is per merchant, in one-second windows, and your API keys share one budget rather than each holding its own. A separate limit sits in front of the API and applies per IP address — a different ceiling from the one this code reports.
| Code | HTTP | When | What to do |
|---|---|---|---|
rate_limited |
429 | The per-merchant rate limit was exceeded. Two limits apply: a global cap on every endpoint (default 30 req/sec) and a stricter cap on invoice creation — POST /v1/invoices (default 5 req/sec). The response says which one tripped and at what limit. |
Read the Retry-After header (the back-off in seconds) and retry after it elapses; most HTTP clients with retry middleware honor it automatically. |
Transport / framework
Returned by the framework error handler when the request never reached a regular handler.
| Code | HTTP | When | What to do |
|---|---|---|---|
malformed_request |
400 | Request body could not be parsed — malformed JSON. | Verify the body is valid JSON. |
payload_too_large |
413 | Request body exceeds the 1 MiB limit. | Send a smaller payload; split large workloads across separate requests. |
unsupported_media_type |
415 | Content-Type is not application/json. |
Set Content-Type: application/json. |
method_not_allowed |
405 | The path exists, but not for this HTTP method. | Use the method documented for the endpoint (e.g. POST to create). |
route_not_found |
404 | No endpoint matches the request path. | Re-check the URL against the API reference. |
Invoices
| Code | HTTP | When | What to do |
|---|---|---|---|
invoice_not_found |
404 | Invoice id resolves to nothing, or to an invoice that belongs to a different merchant. | Use the id returned by POST /v1/invoices verbatim. |
project_not_found |
404 | project_id on a create-invoice request resolves to nothing visible to the caller. |
Use a project id from the dashboard (must be active and owned by the same merchant as the API key). |
invoice_terminal |
410 | Confirm-payment was called on an invoice that's expired or cancelled. (A paid invoice returns invoice_not_awaiting_client, 409 — not this code.) |
Refresh invoice status; nothing more to do. |
invoice_deadline_passed |
410 | Confirm-payment was called after the invoice's deadline. | Create a new invoice. |
requested_amount_invalid |
400 | The invoice amount is missing, malformed, outside the supported decimal range, or not positive. |
Send amount as a positive decimal string with the selected currency and optional network. |
invoice_amount_below_minimum |
400 | Computed USD value of the invoice is below the per-token / per-network minimum from the catalog. | Increase the amount above the minimum surfaced for the chosen token. |
deposit_amount_exceeds_limit |
400 | Computed USD value of the expected payment exceeds the merchant's per-deposit MaxDepositAmount policy cap. Bounds the quoted invoice amount at confirm, not actual on-chain receipts. |
Reduce the invoice amount, or contact support to raise the limit. |
currency_not_enabled_for_project |
400 | The crypto currency requested isn't in the project's enabled-tokens list (any network). | Enable the currency on the project, or pick one from the project's enabled_tokens. |
tokens_required |
409 | Invoice creation or payment confirmation on a project that has no enabled tokens at all. | Enable at least one token on the project in the dashboard. |
acceptance_disabled |
503 | The requested token is not being accepted right now. | Use a different token, or try again later. |
exchange_rate_unavailable |
503 | Rate provider has no fresh quote for the token / fiat pair. | Retry; rates refresh on a short interval. |
network_unavailable |
503 | Auto-confirmation could not use the chosen network. | Drop the auto-confirm; let the customer pick a token via the hosted page. |
payment_method_unavailable |
503 | The chosen token / network cannot take a payment at the moment. Replaced no_available_address, address_lease_failed and bridge_unavailable on 2026-08-23. |
Retry, or let the customer pick a different token or network. |
client_confirmation_failed |
409 | Customer-side confirmation (token selection, auto-confirm) was rejected. The detail field contains the specific reason. |
Inspect detail; common cases: invoice already past awaiting_client, token not allowed. |
invoice_not_awaiting_client |
409 | Confirm-payment was called on an invoice that's no longer in awaiting_client. |
Refresh invoice status; the customer already moved past selection. |
invoice_cannot_be_cancelled |
409 | Invoice is no longer in awaiting_client — the customer already selected a token, or it is already paid or expired. Re-cancelling an already-cancelled invoice returns 200, not this error. |
Inspect current status; cancellation only works in awaiting_client. |
simulate_payment_failed |
409 | Sandbox-only simulate_payment was rejected. |
Inspect detail; usually wrong status or amount mismatch. |
not_sandbox |
403 | simulate-payment (the sandbox simulator) was called on a production invoice. |
Only sandbox invoices can be simulated — use a sandbox invoice, or drive a live invoice with a real on-chain payment. |
Widget invoices
Returned by POST /public/v1/sdk (Low-Code SDK invoice creation).
| Code | HTTP | When | What to do |
|---|---|---|---|
widget_key_missing |
401 | X-Sdk-Key header is absent or empty on an endpoint that requires widget auth. |
Send the public SDK key in X-Sdk-Key. |
widget_key_invalid |
401 | X-Sdk-Key is present but doesn't parse as a Paymos API key id. |
Copy the key from the dashboard; check for stray whitespace, a wrong prefix, or accidental URL-encoding. |
widget_key_not_found |
401 | Key parses but resolves to no active credential — revoked, deleted, or not a Payment-typed key (Payout keys can't drive the widget). | Use an Active Payment key from the dashboard. |
widget_inactive |
403 | The target project's widget is switched off, or its widget was never initialized. | Turn SDK enabled back on in the Integration card on the project's Low-Code page. |
terminal_not_enabled |
403 | Request carried source=terminal, but the project's integration method is not Terminal. |
Create a Terminal project, or send source=embed from a Low-Code project. |
embed_not_enabled |
403 | Request carried source=embed, but the project's integration method is not Low-Code. |
Use the project's own integration method, or create a Low-Code project. |
origin_not_allowed |
401 | The browser Origin doesn't match an allowed-origin policy. The check runs twice — the auth layer matches the origin against the union of every active-widget project on the key, the handler then matches it against the exact target project — and both answer with the same 401, the same code and the same detail. That is deliberate: a different status from the second check would tell a caller holding the (public, scrapable) pk_ key that the origin is registered on some other project of the same merchant, letting staging hosts and unreleased brands be enumerated one probe at a time. |
Add the domain to the widget's allowed origins. Same-origin requests — e.g. a POS terminal served from the same host — skip the check. |
Payment channels
| Code | HTTP | When | What to do |
|---|---|---|---|
payment_channels_disabled |
503 | Payment channels are not enabled for your account in this environment. Every channel and deposit route answers this, including the sandbox simulator. | Contact support to be enabled. Retrying will not change the answer. |
payment_channel_not_found |
404 | The pc_ id resolves to nothing this credential may see — it does not exist, belongs to another merchant, lives in the other environment, or sits in a project outside the key's scope. All four are indistinguishable on purpose. |
Use the id returned by POST /v1/payment-channels verbatim, signed with a key for the same environment and project. |
payment_channel_deposit_not_found |
404 | The pcd_ id resolves to nothing this credential may see. Same four causes as above. |
Use the id from the webhook payload or the polling feed verbatim. |
payment_channel_external_id_invalid |
400 | external_id is empty, blank, or longer than 128 characters — on creation or as a list filter. |
Send a non-blank identifier of at most 128 characters. |
payment_channel_project_has_no_supported_tokens |
409 | The project enables no token a payment channel can collect, so the channel would have no route at all. | Enable at least one supported token on the project, then create the channel. |
payment_channel_simulation_sandbox_only |
403 | simulate-deposit was called on a production channel. |
Simulation exists only for sandbox channels. Drive a production channel with a real transfer. |
payment_key_required |
403 | The request was signed with a Payout (rk_) key. Channel access rides the Payment key, exactly as withdrawals ride the Payout key. |
Sign with a pk_ key of the same environment. |
pagination_cursor_invalid |
400 | A list or feed cursor is malformed, older than 24 hours, or bound to different filters — for the feed, also to a different merchant, environment or project scope. | Restart from the first page. pcd_ deduplication makes a full resynchronization harmless. |
currency_not_enabled_for_project |
400 | The token passed to simulate-deposit is not in the project's enabled-tokens list. |
Enable it on the project, or pick one the channel already returns in its networks[].tokens. |
acceptance_disabled |
503 | Collection for the requested token is currently disabled platform-wide. | Use a different token, or wait until acceptance is re-enabled. |
amount_too_many_decimals |
400 | The simulated amount has more decimals than the token supports. Merchant money is never silently rounded into a different number. | Round the amount to the token's precision before sending it. |
Withdrawals
| Code | HTTP | When | What to do |
|---|---|---|---|
withdrawal_not_found |
404 | Withdrawal id resolves to nothing, or to a withdrawal that belongs to a different merchant. | Use the id returned by POST /v1/withdrawals verbatim. |
insufficient_balance |
409 | Merchant's available balance is below amount + fee. |
Top up, or reduce the withdrawal amount. |
whitelist_required |
403 | The destination address isn't whitelisted for the chosen network. (The network supports whitelisting; this specific address hasn't been added yet.) | Add the address to your whitelist, then retry. |
destination_address_invalid |
400 | Address fails the network's format / checksum validation. | Recheck the address against the network's spec. |
withdrawal_amount_below_minimum |
400 | Amount is below the policy minimum for the chosen token. | Increase the amount above the minimum (returned in detail). |
withdrawal_quota_exceeded |
409 | Active-withdrawals count reached the merchant's policy limit. | Wait for in-flight withdrawals to settle, or contact support to raise the limit. |
withdrawal_amount_exceeds_limit |
400 | The USD-equivalent of the withdrawal principal exceeds the merchant's per-transaction MaxWithdrawalAmount policy cap. |
Reduce the amount, or contact support to raise the limit. |
exchange_rate_unavailable |
503 | The merchant has a MaxWithdrawalAmount cap set, but no fresh token/USD quote was available to check the principal against it — the limit can't be verified, so the request fails closed. |
Retry; rates refresh on a short interval. |
withdrawal_not_enabled |
400 | Paymos doesn't pay out the token/network pair you asked for. detail names the pair — e.g. Withdrawals are not available for USDT on TRC20. Which pairs are payable is our configuration, not yours. |
Withdraw the token on a network Paymos pays out on, or contact support to ask about the pair you need. |
withdrawal_cannot_be_cancelled |
409 | Withdrawal is past the state where it can be cancelled. | Inspect the current status. The window closes when execution starts — still inside created, before anything is signed. |
withdrawal_simulation_failed |
409 | Sandbox-only simulate_completion was rejected. |
Inspect detail. |
merchant_suspended |
403 | The merchant account is suspended for outbound flows, so no withdrawal can be created. | Contact support to lift the suspension. |
outbound_frozen |
503 | Outbound transfers are frozen — either platform-wide, or for the specific network and token you're withdrawing in. | Wait for the freeze to clear, try a different network/token, or contact support. |
withdrawal_network_unavailable |
503 | Payouts of this token on this network can't be settled right now. | Try a different destination network, or retry later. |
Project state
Returned by invoice creation and payment confirmation when the project can't issue invoices in its current state.
| Code | HTTP | When | What to do |
|---|---|---|---|
project_state_invalid |
409 | The project isn't Active — it's Suspended or Archived. The response doesn't say which. |
Check the project's status in the dashboard. An archived project has a Restore project action on its own page; a suspended one is lifted by Paymos, so contact support. Or send the request to a project that's already active. |