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. 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.