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. Append-only: existing codes never change semantics; new codes are added as new scenarios arise.
Switch on code for programmatic handling. detail is English-only and intended for logs / fallback UI; localize on the client by code.
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 — e.g. 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. Limits are counted per merchant, per one-second window — not per IP or per API key.
| 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 | Acceptance for the requested token is currently disabled platform-wide in the catalog. | Use a different token, or wait until acceptance is re-enabled. |
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 tried to reserve an address but the network's address pool / chain status is unavailable. | Drop the auto-confirm; let the customer pick a token via the hosted page. |
no_available_address |
503 | No free pool address for the chosen token / network. | Retry. |
address_lease_failed |
503 | Could not reserve a deposit address. | Retry. |
bridge_unavailable |
503 | The bridge for the chosen network is temporarily unavailable, so confirm-payment couldn't allocate a deposit address on it. | Retry; the outage is transient. |
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 (Widget 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. | Enable the widget in project settings. |
pos_not_enabled |
403 | Request carried source=pos, but the project's widget hasn't enabled POS mode. |
Toggle POS in widget settings, or send with source=embed. |
origin_not_allowed |
401 / 403 | The browser Origin doesn't match an allowed-origin policy. The check runs twice: 401 from the auth layer, where the origin is matched against the union of every active-widget project on the key, then 403 from the handler, where it's matched against the exact target project. |
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. |
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_fee_not_configured |
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 current status; cancellation only works pre-broadcast. |
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. |
hot_wallet_insufficient_liquidity |
503 | Destination network's hot wallet doesn't have enough on-chain liquidity to settle this withdrawal. | Try a different destination network, or contact support. |
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: restore it if archived, activate it if suspended. Or send the request to a project that's already active. |