Error Codes
50 public error codes. Handle any unknown code as api_error —
internal error codes must not be exposed. If you receive a code not listed here, report it to us with the request_id.
Branch on code, not the HTTP status.
Some counterintuitive examples: service_unavailable, limit_exceeded,
asset_not_allowed, state_invalid all use 400, not 5xx.
Standard envelope. Branch on code, not message. message is an English explanation for people, and we may rewrite it at any time. code is the contract; changing it is a breaking change.
{
"type": "invalid_request_error",
"code": "insufficient_balance",
"message": "The member's available balance is not enough.",
"request_id": "01J8X4K9…"
}
Every response includes an X-Request-Id header, and error bodies also include request_id. Start your support ticket with it.
type → HTTP
| type | HTTP | Meaning |
|---|---|---|
invalid_request_error | 400 | Invalid input or unmet business prerequisites |
authentication_error | 401 | Token or signature validation failed |
permission_error | 403 | Identity is valid, but the operation is not permitted |
not_found | 404 | Object does not exist or does not belong to you |
idempotency_error | 409 | Same key with a different body, or the first request is still processing |
rate_limit_error | 429 | Rate quota exceeded |
api_error | 500 | Internal error on our side |
upstream_error | 502 | Upstream unavailable |
upstream_timeout | 504 | Outcome unknown; execution may already have occurred |
⚠ 400 contains two different categories: invalid requests and transactions that cannot proceed. Their retry handling is opposite, so never build a retry policy solely from HTTP status. Use code and the handling instructions below.
Three retry rules, and only these three
· Retry with the same idempotency key: api_error (500), upstream_error (502), upstream_timeout (504), idempotency_in_progress (409), and step_up_required (400, after completing the hosted screen). We may already have completed the operation. A new key == a second real payment. · Retry with a new key: after correcting inputs or adding sufficient funds. Reusing the original key only replays the original failure unchanged. Idempotency means the same key produces the same result, not that the same key eventually succeeds. · Do not retry: request_rejected, insufficient_scope, ip_not_allowed, merchant_disabled, product_not_available, all 404 responses, order_not_cancellable, and state_invalid. Retrying does not change the decision; it only adds noise to your error rate and our logs.
Idempotent replays include X-Idempotent-Replay: true. The idempotency window is 24 hours; afterward, the same key is accepted as a completely new request.
Errors with additional fields
| code | Additional fields |
|---|---|
limit_exceeded | limit_type (single / daily), limit_scope (merchant / member); either may be omitted |
step_up_required | challenge_id, hosted_url, expires_at (Unix seconds) |
rate_limited | retry_after (seconds), also provided in the Retry-After response header |
service_unavailable | retry_after (seconds), only for POST /v1/merchant/deposit-address |
invalid_fields | fields (per-field key + reason), reason; only for POST /v1/remit/payees |
If you receive a code not listed here: handle it as api_error: retry once with the same key, then open a support ticket with request_id if it still fails. Internal codes are not passed through publicly, so either we missed a catalog entry or you reached another service. We need to know in either case.
Quick Reference
| Code | HTTP | Summary |
|---|---|---|
invalid_credentials | 401 | The token-exchange x-client-id / x-api-key is incorrect, the key is disabled or expired, or its rotation overlap window has closed |
invalid_token | 401 | Access token missing, malformed, or expired (30-minute validity), or its key is disabled, expired, or reassigned |
invalid_signature | 401 | Signature mismatch, or any of the three headers x-timestamp / x-nonce / x-signature is missing |
signature_required | 401 | This endpoint requires signing. POST /v1/deposits always requires a signature regardless of key configuration; it is the only entry point that directly creates member balance |
timestamp_out_of_range | 401 | x-timestamp (Unix seconds) differs from our clock by more than ±300 seconds |
nonce_reused | 401 | This x-nonce has already been used within the 5-minute replay window |
environment_mismatch | 400 | The access token’s environment does not match the current deployment |
insufficient_scope | 403 | This API key lacks the scope required by the endpoint |
ip_not_allowed | 403 | The caller’s outbound IP is not on this key’s allowlist |
merchant_disabled | 403 | Merchant entity unavailable. Four internal states share one code: nonexistent / suspended / frozen / closed |
idempotency_key_required | 400 | Write endpoint called without x-idempotency-key |
idempotency_key_invalid | 400 | Idempotency key is not UUID-shaped: hexadecimal 8-4-4-4-12 |
quote_lock_unsupported | 400 | This endpoint prices at execution time; supplying a quote ID does not lock the price |
idempotency_key_reused | 409 | This key was used before, but the request body or target endpoint now differs |
idempotency_in_progress | 409 | The first request using this key is still processing |
member_not_found | 404 | Member nonexistent, or not yours, or suspended; all three produce the same response |
member_context_required | 400 | This endpoint is member-scoped, but x-on-behalf-of is missing |
member_suspended | 400 | This member is suspended |
quick_kyc_disabled | 400 | Quick KYC is not available for this merchant |
quick_kyc_limit | 400 | The member has reached the Quick KYC profile limit |
quick_kyc_exhausted | 400 | No matching unassigned Quick KYC profile |
quick_kyc_unpaid | 400 | Insufficient funds for the Quick KYC usage fee |
kyc_required | 400 | The member’s KYC level is below this product line’s requirement |
insufficient_balance | 400 | The member’s available balance in this asset is insufficient |
merchant_insufficient_funds | 400 | Your prepaid account has insufficient available funds |
merchant_custody_shortfall | 400 | Confirming this withdrawal would make your custody declaration for this asset negative; you are confirming funds you never declared |
product_not_available | 400 | This product is not authorized for you, is no longer on sale, or its activation prerequisites are unmet |
card_operation_not_supported | 400 | This card does not currently support this operation |
limit_exceeded | 400 | A limit was reached. Inspect limit_type (single per transaction / daily daily cumulative) and limit_scope (merchant yours / member the member’s) |
amount_out_of_range | 400 | The amount is outside the product’s permitted range: below its minimum subscription/remittance amount, above its single-transaction maximum, |
request_rejected | 400 | Risk-control rejection. This is the only public risk-control code: no reason, rule name, threshold, or score |
step_up_required | 400 | This action requires the end user to complete strong authentication. The response includes challenge_id, hosted_url, and expires_at |
order_not_cancellable | 400 | The order has passed the point at which it can be cancelled |
asset_not_allowed | 400 | The asset is not on your allowlist, is disabled on the platform, or has no available channel for this asset × network |
invalid_request | 400 | The request itself is invalid: malformed JSON, missing or empty required fields, unknown enums, or incorrect amount format/decimal precision |
kyc_upload_invalid | 400 | Unsupported uploaded file type |
kyc_upload_too_large | 400 | KYC file exceeds the per-file size limit |
invalid_fields | 400 | Per-field validation failed; the response includes a fields array with key and reason for each item |
corridor_not_supported | 400 | We cannot send through this remittance corridor: it is disabled, the jurisdiction is restricted, or this currency/country has no available routing-code type |
resource_not_found | 404 | The referenced object does not belong to this member or does not exist; both produce the same response |
state_invalid | 400 | The action is invalid in the object’s current state: closed card, terminal order, fixed-term Earn without early redemption, non-redeliverable webhook delivery, and similar cases |
duplicate_resource | 409 | An identical record already exists, such as the same withdrawal address added twice by one member |
address_not_allowed | 400 | This address cannot be used for withdrawal. Three cases share one response: our own deposit address, blocklisted address, or invalid format/chain |
qr_code_invalid | 400 | This payment QR code cannot be decoded: unrecognized format, unsupported code standard, or upstream rejection |
not_found | 404 | Path nonexistent, or object nonexistent/not yours. Sandbox-only endpoints also always return this code in live mode |
rate_limited | 429 | Quota exceeded. Includes retry_after in seconds and the Retry-After response header |
api_error | 500 | Internal error on our side |
upstream_error | 502 | Upstream service provider unavailable |
upstream_timeout | 504 | Upstream timeout. Outcome unknown; we may already have completed the operation |
service_unavailable | 400 | This line is temporarily unavailable. The cause may be funding, configuration, or upstream availability; deliberately not distinguished |
Details
invalid_credentials401The token-exchange x-client-id / x-api-key is incorrect, the key is disabled or expired, or its rotation overlap window has closed
In order of frequency: ① Incorrect headers. Token exchange uses the two custom headers x-client-id + x-api-key, not Authorization: Bearer or body fields. An empty value and an incorrect secret produce the same response, so having entered a value does not rule out a misspelled header name. ② Using signing_key as api_key. Each key supplies two values: the former for signatures, the latter for token exchange; they appear next to each other in the console. ③ The key is disabled (not active) or expired. ④ Old credentials have passed the rotation overlap window. This is a hard boundary with no grace period in the code. ⑤ x-client-id and x-api-key come from different keys, or the credentials belong to another deployment environment.
Do not retry in a loop: token exchange itself is limited to 20 requests/minute and will return rate_limited. A nonexistent client and an incorrect secret produce the same response, so do not use it to discover whether a client_id exists. After rotation, old credentials expire strictly at prev_valid_until, without a grace period. Complete the switchover before then.
invalid_token401Access token missing, malformed, or expired (30-minute validity), or its key is disabled, expired, or reassigned
① Expired token: validity is 30 minutes. Caching a token without refreshing according to expired_at, or arbitrarily refreshing once an hour, is among the most common integration errors. ② Incorrect header: both x-auth-token and Authorization: Bearer <token> are accepted. Sending api_key directly as a bearer token also results in this error. ③ The key was disabled, deleted, or expired before the token expired. The key row is read on every request; revocation does not wait for token expiration. ④ The merchant in the token no longer matches the merchant resolved from the key, because ownership changed or sandbox/live tokens were mixed. The latter generally hits environment_mismatch first.
Obtain a new token using POST /v1/connect/token, then resend the original request. Cache and reuse tokens: exchanging one for every request will trigger the token rate limit. If several new tokens still result in 401, check the key status in the console instead of looping indefinitely.
invalid_signature401Signature mismatch, or any of the three headers x-timestamp / x-nonce / x-signature is missing
① Reserialized body: hash the exact raw bytes you send. If your JSON library changes key ordering, whitespace, or escaping, signatures will consistently mismatch and resemble a misconfigured secret. ② Missing query in the second signing component: it is PATH_WITH_QUERY. Signing only /v1/x for /v1/x?a=1 always fails. ③ Any of the three signing headers is missing: x-timestamp / x-nonce / x-signature. Missing headers and incorrect signatures use the same code, so calculating a signature does not prove that all headers were sent. ④ Using api_key instead of signing_key. ⑤ The five components are not joined with newlines, or the method is not uppercase. ⑥ Hashing an empty body as sha256("{}") instead of sha256(""). ⑦ x-timestamp is not numeric. A numeric value in milliseconds produces timestamp_out_of_range instead.
Check each signing component, joined by newlines in this exact order: METHOD (uppercase), PATH_WITH_QUERY (including the query), x-timestamp, x-nonce, and hex(sha256(原始 body)); apply HMAC-SHA256, then base64. The three most common mistakes are: ① using api_key instead of signing_key, which is retrievable and used for signature verification; ② reserializing the body: hash the original bytes actually sent, since reordered keys cause persistent mismatches that resemble an incorrect secret; ③ hashing an empty body incorrectly—the correct hash is e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855. On retry, use a new nonce while keeping the idempotency key unchanged.
signature_required401This endpoint requires signing. POST /v1/deposits always requires a signature regardless of key configuration; it is the only entry point that directly creates member balance
Currently has no emission sites. Missing signing headers produce invalid_signature in the validation function; no code path emits this code. It remains in the catalog to avoid documentation and code repeatedly removing each other’s entries. Mandatory signing for POST /v1/deposits is real; only a dedicated code is absent.
Add the signature as described under invalid_signature. ⚠ The current implementation returns invalid_signature for missing signing headers, so you are unlikely to receive this code. Handling is identical; no separate branch is needed.
timestamp_out_of_range401x-timestamp (Unix seconds) differs from our clock by more than ±300 seconds
① Milliseconds used for the timestamp (13 digits). The value is numeric, so it bypasses invalid_signature and appears here as a difference of many thousands of years. This is much more common than actual clock issues. ② Clock drift on the signing machine: containers or VMs without NTP can drift beyond 5 minutes after weeks without a restart. ③ Local-timezone “seconds” instead of Unix seconds, producing a whole-hour offset. ④ The request spent over 5 minutes in your own queue or retry system, with a timestamp generated when queued.
Enable NTP on the signing machine. Do not reuse the previous timestamp on retries, which turns one clock-drift incident into a persistent failure. After fixing the clock, resend with a new nonce and the same idempotency key.
nonce_reused401This x-nonce has already been used within the 5-minute replay window
① Reusing the previous nonce on retry. The two rules point in opposite directions: change the nonce but retain the idempotency key. Generating them together causes this error on the first retry. ② Deriving the nonce from request contents, such as a body hash or order ID, so every retry of the same transaction has the same value. ③ Multiple processes or containers share a pseudorandom seed and generate the same value in the same millisecond. ④ Your gateway transparently retries and sends the same request twice unchanged. The window is 5 minutes, namespaced by merchant + environment; sandbox and live are independent.
Generate a new nonce for every request, including retries; a random UUID is sufficient. The nonce provides replay protection; fixing it would protect against tampering but not replay. ⚠ Changing the nonce does not mean changing the idempotency key: retries must change the former and retain the latter.
environment_mismatch400The access token’s environment does not match the current deployment
① The cached access token belongs to another deployment environment. ② The token was issued before an environment-rule update.
Obtain a new token in the current environment and cache tokens separately by API address. zinfra.dev is the test environment; zinfra.vip is production. API keys themselves do not have sandbox/live types.
insufficient_scope403This API key lacks the scope required by the endpoint
① A restricted scope was selected: deposits:write / cards:secure. At key creation these only enter the pending-approval list; they may look selected on the key detail page while none are effective. This is easily mistaken for a bug. ② Only read was selected. Write automatically includes read for the same resource, but not vice versa. ③ Misspelled scope key: unrecognized catalog keys are silently discarded during expansion, so the scope is absent without an error. ④ One of our routes lacks a scope declaration; the fallback denies with 403 rather than allowing access. If you are certain all scopes are present, open a ticket with request_id.
Add the scope to this key in the merchant console. It takes effect immediately, without token expiration or renewal: every request reads scopes from the key row; the token payload is only a hint. If you cannot add it, the product line is not enabled for you, which falls under product_not_available; contact your account manager.
ip_not_allowed403The caller’s outbound IP is not on this key’s allowlist
① You changed or added a server, or egress uses a changing NAT pool / serverless runtime with no fixed IP. ② The allowlist uses a prefix length other than /24, /16, or /8, such as /22 or /26. Only those three are recognized; others never match and produce no error. They appear configured but have no effect. ③ Egress uses IPv6. CIDR matching uses dotted-decimal notation; IPv6 requires individual exact addresses. ④ A live key required an allowlist at creation, but you are testing from a local machine or CI.
Do not retry: trying from another machine only creates another security event. Add the new outbound IP in the console. Live keys require an allowlist, so assuming that an unconfigured list means unrestricted access is invalid in live mode. The most common cause is a changed or additional server.
merchant_disabled403Merchant entity unavailable. Four internal states share one code: nonexistent / suspended / frozen / closed
① The merchant is suspended: reads still work; all writes are denied. Being able to read but not write usually indicates an accounting or compliance task awaiting resolution; this is the most common of the four cases. ② Frozen / closed: both reads and writes are denied. ③ The merchant entity cannot be found: the sandbox shadow entity has not yet been created lazily during the first sandbox token exchange, or the token is not for your merchant. ④ A newly introduced merchant status is not recognized here. Unknown values are denied conservatively, never allowed by default.
Do not retry; contact your account manager. ⚠ A useful distinction: suspension allows read-only endpoints but denies all writes. If reads succeed and writes fail, this indicates suspension rather than freezing, typically with an accounting or compliance task awaiting resolution.
idempotency_key_required400Write endpoint called without x-idempotency-key
① Missing x-idempotency-key on a write endpoint. Every write endpoint has idempotency middleware, without exceptions; do not assume an endpoint does not need it. ② Copying another provider’s header name: Idempotency-Key lacks the x- prefix and is not recognized. ③ Putting the key in the body rather than a request header.
All write endpoints—POST / PATCH / DELETE operations that move funds or create objects—require it. Generate the key yourself and persist it; do not derive it on demand from request content, as changing keys on retries defeats idempotency. GET neither requires nor accepts it.
idempotency_key_invalid400Idempotency key is not UUID-shaped: hexadecimal 8-4-4-4-12
① A key built from an order ID plus a timestamp usually does not match the hexadecimal 8-4-4-4-12 shape. ② UUID enclosed in braces or missing hyphens. ③ A ULID, snowflake ID, or base64 random string. ⚠ Validation checks shape, not version bits. The message says UUID v4, but v1/v7 also pass. Switching to v4 is not the remedy if the shape remains wrong.
Use a standard UUID generator. Do not concatenate an order ID and timestamp: it usually fails the required shape, and the timestamp creates a new key on retry.
quote_lock_unsupported400This endpoint prices at execution time; supplying a quote ID does not lock the price
You supplied quote_id to an endpoint that does not support price locking. Currently only POST /v1/merchant/conversions, for internal reallocation of the merchant’s own prepaid assets, returns this code. ⚠ Member exchange no longer returns it. Its price lock now works: call POST /v1/exchange/quotes with lock: true to obtain quote_id, then supply it to POST /v1/exchange/orders to execute at that quote. Expired quotes are always rejected, with no fallback to a new price. Receiving this code for member exchange means you are calling an older version.
Remove quote_id from the request body. For merchant self-conversion, manage exchange-rate risk by checking GET /v1/merchant/conversion-pairs, deciding whether the rate is acceptable, and placing the order immediately. You currently bear movement between the two calls. ⚠ For member exchange, request a lock with lock: true; do not mix these two flows.
idempotency_key_reused409This key was used before, but the request body or target endpoint now differs
① Generating keys per member, day, or order ID rather than per individual call; the member’s second transaction then conflicts. ② Changing a field after a failed response and resending with the original key, such as altering an amount or adding an optional parameter. The request hash covers only the body, not headers. Changing headers does not count; changing one body byte does. ③ Reusing the key at another endpoint, which is also included in the comparison. ④ Declaring the key variable outside a batch loop, causing all requests to share it. ⚠ After the 24-hour window, the same key is accepted as a new request and no longer produces this code.
Use a new key. The request hash covers only the body, not headers; sending the same body to another endpoint and changing a body field are both conflicts. This usually indicates faulty key reuse, such as generating per member rather than per transaction. ⚠ It does not mean the previous request failed. Its result remains available by resending with its original key.
idempotency_in_progress409The first request using this key is still processing
① Two of your processes or machines sent the same transaction concurrently, for example duplicate queue delivery or a scheduled task overlapping manual action. ② You retried immediately after the first request timed out while our original operation was still running; this is normal for fund-moving endpoints. ③ The first request’s final write failed on our side, which is rare. Its key can remain processing until the 24-hour window expires and a new request is accepted. Open a ticket if this persists for several minutes; do not retry indefinitely.
Wait 1~2 seconds, then retry with the same key, using exponential backoff rather than tight loops. Never use a new key: that creates a second real business transaction. This most often occurs when two of your processes send the same transaction concurrently or you retry immediately after a timeout.
member_not_found404Member nonexistent, or not yours, or suspended; all three produce the same response
① Unrecognized x-on-behalf-of identifier. Only two forms are accepted: your external_member_id or our public mem_<uuid>. Email, phone number, and a bare UUID without mem_ are not recognized. ② Sandbox and live use two merchant entities and separate member sets. A member created in sandbox does not exist in live. ③ The member belongs to another merchant. ④ You suspended the member using POST /v1/members/{id}/suspend. ⑤ You blocklisted the member in the console. Blocklisting and suspension are independent flags, both returning this response. ⚠ The latter two are restrictions you applied yourself. Check the member’s console status before investigating code.
Do not retry. Check x-on-behalf-of: either your external_member_id or the mem_<uuid> returned by us is accepted; other forms such as email or phone number are not. The same code appears after you suspend the member with POST /v1/members/{id}/suspend. ⚠ We deliberately do not distinguish these cases, which would otherwise expose a cross-merchant member-discovery endpoint.
member_context_required400This endpoint is member-scoped, but x-on-behalf-of is missing
① Missing x-on-behalf-of on a member-scoped endpoint, often after copying code from a merchant-entity endpoint such as GET /v1/merchant/balances. ② The header is present but empty or whitespace-only; its trimmed value must be nonempty. ③ An intermediate gateway or service mesh stripped an unrecognized custom header. Signature validation often also fails in this case.
Add the header. We never fall back to a default member: such a fallback could move A’s money under B when a header is omitted. Endpoints acting as the merchant entity, such as GET /v1/merchant/balances, do not require it.
member_suspended400This member is suspended
Currently has no emission sites. Suspension is checked while resolving x-on-behalf-of and deliberately maps to member_not_found, together with nonexistent, another merchant’s, and blocklisted members. Distinguishing them would expose cross-merchant member discovery. The catalog entry remains to avoid documentation and code repeatedly removing each other’s entries; do not branch on it.
Restore the member using POST /v1/members/{id}/suspend with suspended: false, then resend with a new key. ⚠ The current implementation does not emit this code: member-scoped suspension is checked during x-on-behalf-of resolution and returns member_not_found. Handle member_not_found instead of adding a branch for this one.
quick_kyc_disabled400Quick KYC is not available for this merchant
Quick KYC is not enabled for this merchant. quick_kyc_enabled on the merchant’s platform-admin record defaults to off.
Do not retry. Ask your account manager to enable Quick KYC, or use POST /v1/kyc/sessions for the member to complete L1 themselves.
quick_kyc_limit400The member has reached the Quick KYC profile limit
The member already has 5 Quick KYC profiles assigned. This per-member limit is strict.
Do not assign another profile. If an existing profile matches the card product, issuance reuses it without another charge. For a card requiring another country/document type, the member must complete their own L1.
quick_kyc_exhausted400No matching unassigned Quick KYC profile
The profile pool contains no unassigned profile matching the card product’s country and document type.
Do not retry with the same criteria. Choose another card product or use the member’s own L1. Only our platform administration can import profiles into the pool.
quick_kyc_unpaid400Insufficient funds for the Quick KYC usage fee
The first assignment costs 10 USDT. Platform-direct operation debits the member’s available balance; downstream merchant_hosted debits your prepaid balance. Insufficient funds fail the entire operation without reserving a profile.
For platform-direct operation, fund the member with USDT; downstream merchants must fund their own prepaid account with USDT. Once sufficient, call again with a new key.
kyc_required400The member’s KYC level is below this product line’s requirement
① The member is at level 0. Members created through the Open API have no KYC, while most product lines require L1. This accounts for almost all cases during integration. ② Cumulative thresholds triggered: earlier transactions succeeded, then one suddenly requires KYC. This is easily mistaken for a bug but is a trigger in the requirements table; remittance and QR payments each have one. ③ The product requires L2, such as personal remittance or some card products, but the member only has L1. ④ For internal transfers, the recipient may lack KYC. The sender can receive this code with complete documentation; the other person needs to act. ⑤ Cardholder information is incomplete: missing fields rather than an insufficient level.
Guide the end user through KYC, then resend with a new key after approval. The response deliberately omits the required level and shortfall. Requirements are a table available from GET /v1/kyc/requirements, which lists required_level by business; do not infer them from errors. Read the current level from kyc_level in GET /v1/members/{id}.
insufficient_balance400The member’s available balance in this asset is insufficient
① The member’s available balance in the asset is genuinely insufficient. Available excludes locked, withdrawal-in-transit, Earn, and card balances; using the total can misleadingly suggest sufficient funds. ② Fees were omitted: principal is covered, but principal plus fees is not. ③ An Earn redemption exceeds holdings; it debits the Earn bucket, not the available balance. ④ QR payment cannot find any debit asset with sufficient funds after trying every asset in payment-preference order. ⑤ The balance no longer covers the new total at remittance reconfirmation, despite covering the original order. ⑥ The branch in POST /v1/deposits is a defensive fallback: reports only increase balance and normally cannot reach it. Open a ticket if it occurs there.
Do not retry. First reconcile against GET /v1/balances; available excludes locked, withdrawal-in-transit, and Earn funds. After funding, resend with a new key, as the same key replays this failure unchanged. ⚠ You report the member balance: if you are certain funds exist but our balance says otherwise, a POST /v1/deposits report is missing rather than our balance calculation being wrong.
merchant_insufficient_funds400Your prepaid account has insufficient available funds
Currently has no emission sites. Insufficient prepaid funds never produce this code: asynchronous order lines, remittance and card issuance, queue without an immediate error; instant-execution lines, QR payments, exchange, Earn, and withdrawals, return service_unavailable. The entry remains to avoid documentation and code repeatedly removing each other’s entries. Follow the action instructions to investigate funding instead of waiting for this code.
Fund the account in the merchant console. ⚠ Actual behavior usually takes two other forms: asynchronous order lines, remittance and card issuance, queue without returning an error when prepaid funds are insufficient; inspect GET /v1/merchant/pending. Instant-execution lines, QR payments, exchange, Earn, and withdrawals, immediately return service_unavailable. Start funding investigations at /v1/merchant/pending and /v1/merchant/balances, not with error codes.
merchant_custody_shortfall400Confirming this withdrawal would make your custody declaration for this asset negative; you are confirming funds you never declared
The only emission site is POST /v1/withdrawals/{id}/confirm, the confirmation step of two-phase withdrawals. ① An on-chain deposit was never reported through POST /v1/deposits. The member balance came from another source, such as migrated history or an operations adjustment, without a corresponding custody declaration. ② The report used the wrong asset, placing the custody declaration under another asset. Each asset is accounted for separately; assets do not offset each other. A sufficient aggregate reported amount is not evidence against this error. ③ One of our product lines omitted the merchant custody leg. Our side will have an invariant alert; open a ticket with request_id.
Do not retry this transaction; submit the missing deposit reports first. Report on-chain receipts missing from POST /v1/deposits, then confirm this withdrawal. ⚠ Distinguish this from merchant_insufficient_funds: that requires funding your prepaid account; this requires completing deposit reports. No amount of prepaid funding resolves the latter.
product_not_available400This product is not authorized for you, is no longer on sale, or its activation prerequisites are unmet
① The product record itself is unavailable: an Earn product is delisted, ended, paused, or not yet on sale, all sharing one code; this exchange direction is disabled (USDT→USD and USD→USDT are independent records); internal transfers are globally disabled; or the withdrawal asset × network has no channel. ② Card issuance: BIN capacity exhausted, inventory empty, unsupported country, product not authorized for you, quick applications disabled, or unavailable provider. ③ This remittance line, express / personal, or this asset is not available to you. ④ The personal remittance line requires an upstream subaccount for the member, which is absent. ⚠ A disabled merchant product-line switch returns service_unavailable, not this code. Both require your account manager, but investigation starts in different places.
Do not retry. Ask your account manager to enable it, or inspect enabled lines with GET /v1/merchant/lines. ⚠ enabled and halted are independent flags: the latter is an automatic low-funding pause that resolves automatically after funding; only we can change the former. An SDK collapsing them into one flag mistakes a funding-resolvable pause for a disabled product line.
card_operation_not_supported400This card does not currently support this operation
The current card product does not enable the requested operation.
Do not retry the same operation. Use the card’s enabled features or confirm the supported operations with your account manager.
limit_exceeded400A limit was reached. Inspect limit_type (single per transaction / daily daily cumulative) and limit_scope (merchant yours / member the member’s)
① Member daily cumulative amount or transaction count, with separate limits for six lines. This is the most common case: orders work in the morning but not in the afternoon. ② Single-transaction limit (limit_type: single). ③ Merchant-side checks on POST /v1/deposits: per-transaction and daily cumulative limits with limit_scope: merchant. They prevent a compromised key from creating unlimited balance; normal activity should not reach them. Investigate possible abuse first, then consider an increase. ④ Record count rather than amount limits, such as withdrawal and deposit address books. Splitting amounts does not help. ⑤ Cards: cardholder count, quick-application count, or card limits. ⚠ Both additional fields may be omitted. Do not assume a member limit when limit_scope is absent.
single → split into smaller amounts and resend with new keys. daily → stop retrying that day and return the next day. limit_scope: merchant comes from your merchant configuration; ask your account manager to adjust it. limit_scope: member is a platform admission limit that merchants cannot change. Inspect current usage through GET /v1/members/{id}/limits to explain it to the end user. ⚠ The error body does not include the threshold. Do not guess; that endpoint is the source of truth.
amount_out_of_range400The amount is outside the product’s permitted range: below its minimum subscription/remittance amount, above its single-transaction maximum, or below the minimum ledger unit after fees.
① Below the product’s minimum subscription, top-up, or remittance amount. Small one-unit integration test requests commonly hit this. ② Above the product’s maximum transaction amount. This differs from the single-transaction allowance in limit_exceeded: a product range can be satisfied by changing the amount, whereas an allowance may require waiting or an increase. ③ Fees consume the principal, or the conversion yields less than one minimum ledger unit; small exchange and remittance tests often hit this. ④ Earn, internal transfers, and QR payments each have min/max ranges configured separately from allowances. Increasing an allowance does not make this transaction valid.
Change the amount and resend with a new key. Handling differs from limit_exceeded: that means the allowance is exhausted and there is no point retrying that day; this can be resolved by changing a number. Find the range on the relevant product query endpoint, such as min_amount / max_amount in GET /v1/earn/products/{id}.
request_rejected400Risk-control rejection. This is the only public risk-control code: no reason, rule name, threshold, or score
① The member is restricted: account frozen, closed, restricted, or blocklisted. This is currently the most common cause because all risk rules default to disabled and cannot match until enabled. ② The upstream explicitly rejected this transaction, such as card top-up, card withdrawal, freeze/unfreeze, or binding. The upstream is reachable and has made a decision; retrying does not change it. ③ A risk rule matched block, which is only possible after the corresponding rule is enabled in our administration console. ④ The recipient is blocklisted: a remittance payee or internal-transfer recipient member. ⑤ A document number is already used elsewhere. Cross-merchant existence information is collapsed into this code without identifying the conflict. ⚠ The decision criteria are never exposed, so the response cannot distinguish these cases. First inspect the member’s restriction status in your merchant console; that is the part visible to you.
Never retry. Repeating the request does not change the decision; it only adds another record that may make the member appear more suspicious. Do not infer which rule matched; those details are never exposed. Appeal through a merchant-console support ticket with request_id. ⚠ Banned, blocklisted, or frozen members also map to this code, so previously successful use is not contradictory.
step_up_required400This action requires the end user to complete strong authentication. The response includes challenge_id, hosted_url, and expires_at
① The endpoint inherently requires strong authentication, such as adding a withdrawal address, activating a card, or obtaining a card-secret viewing ticket. The first call always receives this code; it is a workflow step, not an error. ② The ticket expired after 5 minutes or has already been used once, while the retry supplied the old challenge_id. ③ The ticket’s member or action does not match the request: a freeze-card ticket cannot reveal card secrets. ④ The end user did not actually finish factor verification on the hosted screen; the ticket’s passed flag remains 0. ⑤ The exchange direction is configured to require step-up. The current Open API rejects this case and does not return challenge_id / hosted_url, because there is no hosted flow for it. A step_up_required without those fields identifies this case. Ask us to disable step-up for that direction before using it.
Open hosted_url for the end user, using our hosted screen with all factor checks within our domain. After completion, resend the same request body with the same idempotency key, adding x-step-up: <challenge_id>. This is the only error for which resending the same key actually reruns execution; the idempotency layer explicitly permits it. Tickets expire after 5 minutes, are single-use, and are bound to the member and action. Obtain a new ticket after expiry. ⚠ Self-attestation in a request body, such as step_up_passed: true, is never accepted; do not attempt it.
order_not_cancellable400The order has passed the point at which it can be cancelled
Only two emission sites: POST /v1/withdrawals/{id}/confirm and /fail. ① The transaction has already reached the other terminal state: confirming then marking failed, or vice versa. ② Concurrent processes issue confirm and fail for the same transaction; the losing one receives this code. ③ The transaction never entered a state eligible for advancement. ⚠ Repeating the same terminal state returns an idempotent replay with replayed: true, not this code. ⚠ A remittance past its cancellation point returns state_invalid, not this code.
Do not retry. Retrieve the current order state before deciding what to do next. ⚠ In two-phase withdrawal confirmation, concurrent advancement to the other terminal state also returns this code. First GET the order: if it already has your intended state, the operation succeeded and needs no further action.
asset_not_allowed400The asset is not on your allowlist, is disabled on the platform, or has no available channel for this asset × network
① The asset is outside your deposit allowlist. ⚠ Allowlists restrict access: no entries means all enabled assets are usable; after the first entry is added, every unlisted asset is denied. This transition is a common trap: previously available assets can suddenly become unavailable. ② The platform has not enabled the asset. ③ The asset × network has no available channel, commonly during deposit-address creation when network does not match our channel table. ④ The asset code includes a suffix or alias, such as USDT.BSC. Case is ignored because we uppercase it. ⑤ Card top-ups or internal transfers are separately disabled for this asset.
Do not retry. Use GET /v1/assets for the assets currently available to you. Merchants without an allowlist see all enabled assets: the allowlist restricts access rather than granting it. Ask your account manager to add an asset; we add one record, without requiring your release.
invalid_request400The request itself is invalid: malformed JSON, missing or empty required fields, unknown enums, or incorrect amount format/decimal precision
In order of frequency: ① Amount format: fixed-point strings are mandatory, with decimal places equal to the asset’s ledger_scale. "12.5" is invalid for a 6-decimal asset; use "12.500000". JSON numbers also produce this code. ② A required field is missing or empty, such as external_member_id, asset, reference, or payee_id. ③ Incorrect ID shape: after removing its prefix, it must match our identifier format; invented values fail. ④ Invalid JSON body: an empty write body or form-encoded content. ⑤ Unrecognized enum values, such as renewal mode, recipient type, or sandbox upstream/behavior names. ⑥ Both or neither of two mutually exclusive parameters are supplied, such as payout_amount and source_amount in quotes. ⑦ Email already belongs to another merchant’s member. To conceal cross-merchant existence, this also uses the generic code, without saying “already registered”. If member creation repeatedly returns invalid_request despite correct fields, this is a likely cause. ⚠ The response does not identify the field. During integration, check each endpoint’s parameter table rather than guessing.
Correct the request and resend with a new key. This is the most frequent code in the catalog, and it does not identify the field; compare against each endpoint’s parameter table instead of guessing. ⚠ Amount errors are common: amounts must be fixed-point strings with decimal places equal to the asset’s ledger_scale, available from GET /v1/assets. "12.5" is invalid for a 6-decimal asset; use "12.500000". JSON-number amounts produce the same error.
kyc_upload_invalid400Unsupported uploaded file type
The KYC file is empty or its magic bytes do not identify JPEG, PNG, WebP, or PDF. The declared Content-Type does not determine acceptance.
Read the original file bytes again and upload with a new idempotency key. Do not upload base64 text or a JSON wrapper.
kyc_upload_too_large400KYC file exceeds the per-file size limit
A single KYC file exceeds 8 MB.
Compress it to within 8 MB without compromising document legibility, then upload with a new idempotency key.
invalid_fields400Per-field validation failed; the response includes a fields array with key and reason for each item
① Field validation for payee creation, POST /v1/remit/payees: IBAN, routing code, account-holder name, purpose code, or address. Only this endpoint includes a fields array. ② Mistyped withdrawal address: invalid format, EIP-55 checksum, zero address, or chain/asset mismatch. These deliberately do not use address_not_allowed: a typing error should not tell users their address is prohibited or suggest they are blocked. ③ Invalid amount fields, such as amount in exchange / transfers / Earn, are syntax errors like ②. ④ Three card-limit checks: incorrect hierarchy, above the product maximum, or too small to be meaningful. ⑤ Cards: missing or invalid shipping address, mismatched activation details, or incorrect card number/expiry/CVV during binding. ⑥ The chosen payee does not match this order’s corridor. The payee selection is wrong; the corridor itself may work, so this is not corridor_not_supported. ⚠ All cases except ① omit fields; your parser must handle its absence.
Present each fields entry to the end user, then resend corrected input with a new key. ⚠ Currently only POST /v1/remit/payees includes fields; other endpoints’ field errors map to invalid_request, which has no field list. Do not generically read fields for every 400; handle its absence.
corridor_not_supported400We cannot send through this remittance corridor: it is disabled, the jurisdiction is restricted, or this currency/country has no available routing-code type
① We have not enabled the corridor, or its destination jurisdiction is restricted. ② This currency × country has no available routing-code type, such as supplying a local clearing code for an IBAN-only country. ③ No funding channel is currently available for the corridor. ④ For QR payments, the upstream says the corridor is unavailable. Ten different codes would have the same result. This deliberately differs from qr_code_invalid: repeatedly asking the user to rescan is the most common incorrect handling.
Choose another corridor or payment method; do not retry the original parameters. ⚠ The distinction from product_not_available is operational: that means remittance is not enabled for you and requires your account manager; this means remittance is enabled but the destination is unsupported, so change the destination or use SWIFT.
resource_not_found404The referenced object does not belong to this member or does not exist; both produce the same response
① An object referenced in the body does not belong to the member in x-on-behalf-of. Payees, orders, cards, and Earn positions are queried by member ownership. If the member is wrong, the object appears absent; the incorrect value is the member, not the object. ② The ID belongs to another resource type or has a missing/extra prefix: pye_ payee / rmt_ remittance / crd_ card / ern_ Earn. ③ An internal-transfer recipient belongs to another merchant. The response always says not found, exactly as if nonexistent. ④ The object genuinely does not exist, such as a deleted payee or an order never created. ⚠ Missing objects referenced through path parameters usually return not_found, not this code. Handle both.
Do not retry. Check ID prefixes (pye_ payee, rmt_ remittance, crd_ card, etc.) and whether x-on-behalf-of identifies the object’s owner. ⚠ We deliberately do not distinguish nonexistent objects from objects you do not own; doing so would expose an ID-discovery endpoint.
state_invalid400The action is invalid in the object’s current state: closed card, terminal order, fixed-term Earn without early redemption, non-redeliverable webhook delivery, and similar cases
① Acting on an object already in a terminal state, such as cancelling a settled remittance, redeeming ended Earn, or redelivering a delivered webhook. Retries and concurrency are common causes. ② Expired quote: exchange quotes last just over a minute; a user lingering on confirmation can exceed that time. This frequently recurs during integration. ③ Remittance reconfirmation: the confirmation window expired, the quote changed again, the order is not awaiting confirmation, or reconfirmation rounds are exhausted. ④ Clients cannot redeem fixed-term Earn early. This is a product rule, not temporary unavailability; do not retry. ⑤ Card state disallows the action: closed, transitioning through freeze/unfreeze, under fund protection, unpaid penalty debt, expired application, or activation before shipment. ⑥ Withdrawal/remittance state claiming failed: not in transit, never dispatched, no locking journal, too recent to act, or already has a settlement journal. ⚠ The response does not expose internal status names. First GET the object, then decide the next step.
First GET the object and decide based on its current state; do not retry blindly. ⚠ The response does not expose internal status names. The internal state machine is not a public contract; only the public statuses listed in endpoint documentation can be relied on.
duplicate_resource409An identical record already exists, such as the same withdrawal address added twice by one member
① The same member adds the same withdrawal address twice. A database unique index enforces this, so concurrent “check then insert” implementations can both attempt insertion, with one receiving this code. ② The member already has a card application in progress and starts another. ⚠ Unlike idempotency_key_reused, this is a business duplicate. Changing the idempotency key still fails.
Do not retry or use a new key; it still conflicts with the same record. List existing records and use that one. ⚠ Unlike idempotency_key_reused, this is business-level duplication, independent of your idempotency key.
address_not_allowed400This address cannot be used for withdrawal. Three cases share one response: our own deposit address, blocklisted address, or invalid format/chain
① The address is one of our own deposit addresses, commonly caused by confusing deposit and withdrawal directions or using an address retrieved from our API as a withdrawal destination. ② The address is blocklisted. ⚠ Both produce the same response; distinguishing them would reveal an oracle for discovering our addresses. ⚠ Mistyped addresses—format, checksum, zero address, or chain mismatch—produce invalid_fields, not this code. Asking users to check for a mistyped character after this code is unhelpful; that is not the cause.
Use another address. Do not infer the exact cause from the response; the cases are combined to avoid exposing a discovery tool for our addresses. If the user insists the address is valid, open a ticket with request_id. ⚠ The withdrawal address book is the only address source in the main flow; additions require strong authentication and a cooling period. This error cannot be bypassed through another entry point.
qr_code_invalid400This payment QR code cannot be decoded: unrecognized format, unsupported code standard, or upstream rejection
The sole cause is that the QR payload cannot be decoded: unrecognized format, unsupported code standard, or upstream rejection. ① The scanned payment code is of an unsupported type. ② The payload changed in transit: truncation, added newlines/spaces, or an extra URL-decoding pass. ③ A decoder misread characters from a code displayed on a screen. ⚠ If the upstream reports an unavailable corridor, the code is corridor_not_supported, not this one. Rescanning cannot resolve that case.
Ask the end user to rescan or use another code. Do not retry the same payload. ⚠ Support varies by region and QR standard; a previously successful payment does not guarantee the current one is supported.
not_found404Path nonexistent, or object nonexistent/not yours. Sandbox-only endpoints also always return this code in live mode
① Misspelled path or missing /v1 prefix; unmatched /v1/* paths map here. ② Object not found through a path parameter: card, card application, remittance, withdrawal, Earn order, payee, or address-book entry. These endpoints use this code rather than resource_not_found. Usually the ID does not belong to the member in x-on-behalf-of or to your merchant. ③ A sandbox-only endpoint was called in live mode. 404, not 403: it should not exist in production, and 403 would acknowledge its existence. ④ Wrong ID prefix: crd_ / rmt_ / wdr_ / ern_, etc.
Do not retry. Check the path spelling and /v1 prefix. ⚠ Sandbox endpoints return 404 rather than 403 in live mode: it is not a permission issue; they must not exist in production.
rate_limited429Quota exceeded. Includes retry_after in seconds and the Retry-After response header
① Exchanging tokens too frequently, over 20 requests/minute. Requesting a token for every API call triggers this yourself. Cache and reuse it until expired_at; this dominates integration-time cases. ② Polling read-only endpoints, over 1200 requests/minute. This generally means treating us like a database to poll; consume webhook events instead. More threads only hit the limit faster. ③ Order operations allow 120 requests/minute, member writes 300/minute, and POST /v1/deposits 60/minute. Bulk member imports and historical deposit backfills can hit the latter two. ④ Exhausted card-binding attempts also return 429. This is a brute-force protection gate; backoff does not resolve it, and a different action is required. ⚠ Quotas apply per merchant, not per key. More keys do not increase capacity.
Back off according to Retry-After and add random jitter, so clients waking simultaneously do not turn one burst into continuous overload. Quotas are per merchant, not per key; extra keys do not add capacity. Current buckets: POST /v1/deposits 60/minute, member writes 300/minute, order operations 120/minute, reads 1200/minute, and token exchange 20/minute. ⚠ Hitting the read bucket usually means treating us as a database to poll. Consume webhooks rather than adding threads.
api_error500Internal error on our side
① We omitted an internal error-code registration. Unregistered codes map here, misrepresenting a valid business rejection as an internal error. It is recognizable: the same parameters always produce 500, regardless of retry count. Consistently reproducible 500s on the same endpoint and parameter category are almost always this case; a support ticket helps more than retries. ② An unhandled exception in our business path: unavailable dependency, SQL error, or unexpected upstream response shape. ③ Genuine transient instability, which is less common.
Retry with the same idempotency key: we may have completed part of the operation, and a new key risks executing twice. If two or three backed-off retries fail, open a ticket and include request_id, our only handle for locating this call. ⚠ Repeated 500s on the same endpoint and parameter category usually indicate a real defect rather than instability. Report it promptly instead of absorbing it through retries.
upstream_error502Upstream service provider unavailable
① Our issuing provider is busy or an outbound call temporarily failed, during issuance, activation, or card top-up. This is the one category where retrying unchanged may succeed. ② QR payment upstream 5xx, network failure, or failure to obtain a quote. ③ Remittance payee creation failed upstream. ⚠ This is an upstream issue, not incorrect payee data. We deliberately avoid invalid_fields so you are not directed to edit correct information. ⚠ Upstream identities are never exposed. If the issue persists for several minutes, open a ticket.
Retry with the same idempotency key and exponential backoff. Open a ticket if it persists for several minutes. We can identify the affected provider; you cannot, because upstream names are not exposed publicly and our supplier structure is not public information.
upstream_timeout504Upstream timeout. Outcome unknown; we may already have completed the operation
No current code path emits this code. An actual 504 comes from the gateway in front of us, such as an edge timeout or interrupted connection, rather than a business-path decision. It therefore conveys only one fact: the outcome is unknown. The catalog retains it to avoid documentation and code repeatedly removing each other’s entries, and because its action rule—retry with the same idempotency key—also applies to gateway timeouts. Violating that rule is the most costly mistake described in this documentation.
Retry with the same idempotency key, or first verify using the order query endpoint. A new key == a second real payment; this is the most costly mistake in this documentation. ⚠ The idempotency window is 24 hours. After it expires, replaying the same key is equivalent to using a new one, leaving manual reconciliation as the only option. Do not delay.