Skip to content

API

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 callforbidden, 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 statewhitelist_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 is whitelist_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.