Errors
This page is the master reference for how a call to /api/v1/partner or /api/v2/public fails:
which HTTP status you get, which of three body shapes carries it, and every machine-readable
code the platform raises, with the condition and the remedy. Per-operation pages in the
reference repeat the codes that apply to them; this page is derived from the same code paths.
Branch on the HTTP status first
| Status | Meaning | Body shape |
|---|---|---|
400 |
The request itself is malformed: an X-Tenant-Id header was sent, X-Correlation-Id is invalid, Idempotency-Key is missing or malformed on a mutating call, or the JSON body could not be bound |
problem+json (shape 3) |
401 |
The API key was not accepted | problem+json |
403 |
The key is valid but is not allowed to do this: missing scope or IP outside the allowlist (problem+json), or a policy gate refused the customer — compliance hard block, account not offered (flat ErrorResponse, shape 2) |
problem+json or shape 2 |
404 |
The resource does not exist — or exists but belongs to a customer this key cannot act for. These are indistinguishable on purpose (see Authentication); code is always null |
shape 2, code: null |
409 |
The request conflicts with current state. Three families: an operation refusal (beneficiary_not_ready, exchange_order_failed, … — shape 1), a compliance HOLD that a review can clear (shape 2), or an Idempotency-Key conflict (problem+json) |
shape 1, 2 or 3 |
413 |
A JSON request body exceeded the 1 MiB bound the replay layer can fingerprint safely (idempotency_request_too_large); the operation did not run and no key state was written |
problem+json |
422 |
Refused — either the body failed validation (invalid_amount) OR a state precondition is not met with a perfectly valid body (insufficient_funds, beneficiary_not_found). Three 422s occur on GETs with no body at all (unsupported_asset, unknown_level, and invalid_pair on /transfer/price). Read data.code; many are fixable outside the request |
shape 1 |
429 |
Too many requests — one of three causes (below). Honour Retry-After |
problem+json |
500 |
Unexpected failure. code is absent. Quote X-Correlation-Id to support |
problem+json |
503 |
A dependency the operation needs is unavailable. On the two crypto reads (…/crypto/wallets, …/crypto/wallets/{asset}/balance) it is a transient custody-provider refresh failure, provider_balance_refresh_failed in shape 1 — retry with backoff; elsewhere the operation's reference names the cause |
shape 1 or 2 |
Which check runs first
Shared middleware refuses in a fixed order, and the response only ever reflects the EARLIEST failing check. Fix problems in this order when debugging a client:
X-Correlation-Idmalformed or sent twice →400 correlation_id_invalidX-Tenant-Idpresent at all (even empty) →400, no code — fires even when the key is also wrong- API key →
401/403, or429if your IP has too many recent failures - Rate limits (per route, then tenant aggregate) →
429 rate_limited Idempotency-Keymissing or malformed (mutating calls) →400 idempotency_key_invalid- Replay layer: a JSON body over 1 MiB →
413 idempotency_request_too_large; then anIdempotency-Keyconflict →409 idempotency_key_reused/idempotency_key_in_progress/idempotency_response_unavailable - Body binding →
400with anerrorsmap, no code - The operation itself → its own
404/409/422/403
The three body shapes
1. The envelope — every 409 and 422 the operation itself raises, and the 503 on the crypto reads:
{
"success": false,
"status_code": 422,
"message": "Invalid conversion pair.",
"data": { "code": "invalid_pair", "detail": "from_currency and to_currency are required and must be different." }
}
data.code is always present. success and status_code repeat the HTTP status; message is
human-readable and not stable.
2. The flat ErrorResponse — every 404, and the 403/409 raised by policy gates:
{ "title": "Customer was not found.", "detail": "No customer exists for the authenticated tenant and customer id.", "status": 404, "code": null }
code is null on every 404 and populated only by the gates listed under
Policy-gate codes.
3. RFC 7807 application/problem+json — written by shared middleware for 400, 401, 403 (scope/IP),
409 and 413 (idempotency) and 429, and by the framework for the body-binding 400 and for 500:
{ "type": "https://tools.ietf.org/html/rfc9110#section-15.6.4", "title": "Too many requests.", "status": 429, "detail": "Request rate limit exceeded.", "code": "rate_limited" }
Note the content type: a strict application/json parser must accept application/problem+json too.
The body-binding 400 adds an errors object (field → messages) and carries no code.
Codes carried by problem+json
The shared replay layer supplies the idempotency 409/413 codes below. The public end-customer
POST .../internal-transfers and POST .../consolidate operations bypass it and use their own
transactional recovery and JSON conflict codes; common header validation still applies. See
Sub-customers & parent access.
code |
Status | Condition | Remedy |
|---|---|---|---|
correlation_id_invalid |
400 | X-Correlation-Id sent twice, or longer than 128 visible ASCII characters |
Send one value, ≤128 chars, printable ASCII — or omit it and use the one we return |
| (none) | 400 | An X-Tenant-Id header was present. Title Invalid tenant header. |
Remove the header; the tenant comes from the key |
idempotency_key_invalid |
400 | Mutating call with no Idempotency-Key, two of them, or a value outside 8–256 visible ASCII (no spaces) |
Send exactly one well-formed key |
(none), errors map |
400 | The JSON body could not be bound (malformed JSON, wrong type, bad UUID) | Fix the field named in errors |
api_key_authentication_failed |
401 | Missing, malformed, unknown, revoked, expired, rotated-out, not-yet-active, wrong-environment key; or the tenant is suspended / API access is off. detail is specific only for request-shape mistakes (no key, both headers, non-Bearer scheme, unparseable key) |
Fix the credential. Do not retry in a loop: repeated failures from one IP become 429 |
api_key_not_authorized |
403 | The key lacks a scope this operation requires, or the request came from an IP outside the key's allowlist | Compare GET /api/v1/partner/me scopes with the operation's security list; issue a new key with the right scopes or allowlist |
idempotency_key_reused |
409 | The key was already used with a different method, path, body, or caller — a second API key of the same tenant counts as a different caller | Mint a new key for the new action |
idempotency_key_in_progress |
409 | The first attempt with this key is still executing | Retry the identical request shortly; you will get its stored result. A refusal that persists means the original completion could not be recorded — treat it as a support case, not a cue to mint a new key |
idempotency_response_unavailable |
409 | The original request returned 2xx, but its response was too large to retain for replay — while the completed entry exists, an identical retry is refused rather than re-executed. This is not proof of payment settlement | Confirm the original result by reading the resource (see Idempotency); use a new key only for a genuinely new action |
idempotency_request_too_large |
413 | A JSON body above 1 MiB on an idempotency-protected route. The operation did not run and no key state was written | Split the payload or use a documented bulk operation |
rate_limited |
429 | (a) per-route budget for this key, (b) tenant-wide aggregate, or (c) too many failed authentications from your IP — detail is Too many failed authentication attempts. for (c) |
(a)/(b): wait Retry-After seconds. (c): waiting does not help — fix the credential |
| (none) | 500 | Unhandled failure | Read the resource's state first, then follow its retry contract. Once failure is recorded, shared replay allows identical bytes and key to execute again; changed bytes conflict while retained. Transactional commands use their own recovery rules |
Authentication codes are intentionally one per status: the granular reason (expired vs revoked vs IP-denied) is recorded in our audit trail and never returned, because it would tell a probe how far its guess got.
Idempotency retention and size boundary
Shared replay entries are retained for 24 hours. Except for the transactional commands above,
on JSON requests the shared replay layer
fingerprints bodies up to 1 MiB (1,048,576 bytes) — including chunked bodies, which are read
through a bounded buffer rather than let through unprotected. A JSON body above that bound is
refused with 413 idempotency_request_too_large before the operation runs. A successful response
above the same 1 MiB boundary is not retained for byte-for-byte replay, so an identical retry is
refused with 409 idempotency_response_unavailable instead of being re-executed while that completed
entry remains retained. After expiry the shared replay layer can let the operation run again;
payment workflows may have additional durable checks. A first attempt
whose non-2xx failure is recorded does not lock the key — but only the identical bytes may retry it; a
changed body under a failed key is refused with 409 idempotency_key_reused. The full contract is
in Idempotency.
Operational response headers
- A valid
X-Correlation-Idsupplied by the caller is echoed in the response. If it is omitted, Kwiikpay supplies and returns one; it is on every response, including successes — include it in support requests. - A
429response usesRetry-After(whole seconds) to say when to retry. - A
401response carriesWWW-Authenticate: Bearer. - The API does not emit
X-RateLimit-*headers. Do not build quota logic around them.
Policy-gate codes (flat ErrorResponse)
These are raised by shared gates rather than by one operation, on the money-moving routes (withdrawals, conversions, crypto withdrawals) and account opening.
code |
Status | Condition | Remedy |
|---|---|---|---|
compliance_subject_blocked |
403 | The customer is blocked, suspended, rejected, expired or provider-revoked by compliance | Permanent for the current state; contact support |
compliance_subject_manual_review |
409 | The customer is in manual review | Retry once the review clears (watch onboarding status) |
compliance_subject_lifecycle_unavailable |
409 | The compliance state could not be read | Retry later |
compliance_subject_lifecycle_unknown |
409 | The compliance state is unknown | Retry later; contact support if it persists |
fiat_unsupported_jurisdiction |
403 | Fiat account opening: the customer's country is on the blocked list (see the v1 reference introduction) | Cannot be changed by configuration |
currency_not_available_for_customer_type |
403 | Fiat account opening: this currency is not offered to this customer type | Choose an offered currency |
self_serve_provisioning_disabled |
403 | Fiat account opening: self-serve provisioning is locked for this customer — operations opens accounts on request. Checked first inside the account-opening workflow, before the offer policy | Contact support |
| (null) | 404 | Unknown id, or an id this key cannot act for | Check the id and the key's binding via /me |
A platform-wide money-movement pause is NOT reported with its own code on v1. The shared handler
defines 409 money_movement_frozen, but the three v1 money routes catch it first and answer with
their generic failure code (fiat_withdrawal_failed, crypto_withdrawal_failed,
exchange_order_failed, shape 1) whose detail is operator-authored text. Treat those three codes
as "retry later" unless detail says otherwise.
Operation codes (envelope, data.code)
Status is 422 unless marked 409. Every customer-scoped route also returns 404 (shape 2,
code: null) for an unknown or unreachable customerId, and every money-moving route starts with the
shared precondition 409 kyc_or_kyb_not_approved (the customer has not reached an approved
verification status — read GET /customers/{customerId}/onboarding/status).
Customers and onboarding
code |
Operation | Condition | Remedy |
|---|---|---|---|
invalid_email |
PATCH /customers/{customerId} |
email is not a valid address |
Fix the field |
unsupported_status |
PATCH /customers/{customerId} |
status is not a value the platform accepts |
Use a documented status |
jurisdiction_invalid |
PATCH /customers/{customerId} |
jurisdiction is not an ISO 3166-1 alpha-2 code |
Send a two-letter country code |
external_reference_in_use (409) |
PATCH /customers/{customerId} |
The email or external reference is already assigned to another customer in your tenant — the message never says which | Choose a different reference |
unknown_level |
GET /onboarding/requirements |
The requested verification level does not exist | Use a level from the reference |
invalid_onboarding_type |
POST …/onboarding/kyc/initiate, …/kyc/token, …/kyb/initiate, …/kyb/token |
KYC was requested for a business, or KYB for an individual | Use the flow matching customerType |
password_not_supported |
POST /customers (whitelabel programme) |
password was sent; no identity account exists on this surface, so a credential would be discarded |
Remove the field — never send a real credential |
Exchange (conversions)
code |
Operation | Condition | Remedy |
|---|---|---|---|
invalid_pair |
GET /transfer/price, POST …/banking/conversion-requests |
pair is not SOURCE/TARGET, or the two currencies are missing or identical |
Send exactly one slash and two different assets |
exchange_quote_unavailable (409) |
GET /transfer/price |
The pricing provider did not return a quote | Retry shortly |
invalid_amount |
POST …/banking/conversion-requests |
amount_minor is not a positive integer |
Send minor units as an integer > 0 |
below_minimum_trade_size |
POST …/banking/conversion-requests |
The amount is under the minimum trade size for the source asset | Raise the amount (the detail names the floor) |
insufficient_onchain_balance |
POST …/banking/conversion-requests |
Selling crypto: the on-chain balance is lower than the amount | Wait for pending deposits to settle, or lower the amount |
exchange_account_missing / exchange_wallet_missing (409) |
POST …/banking/conversion-requests |
The customer has no fiat account / crypto wallet on one side of the pair | Open the account or wallet first |
exchange_funding_account_ambiguous (409) |
POST …/banking/conversion-requests |
More than one funding account matches | Contact support |
exchange_ledger_prerequisites_missing (409) |
POST …/banking/conversion-requests |
Ledger accounts for this customer are not provisioned | Contact support |
exchange_workflow_unavailable (409) |
POST …/banking/conversion-requests |
The exchange workflow is not enabled on this host | Contact support |
crypto_balance_unverifiable (409) |
POST …/banking/conversion-requests |
The custody provider could not confirm the balance | Retry later |
crypto_network_unavailable (409) |
POST …/banking/conversion-requests |
No active network for the asset | Retry later; contact support if it persists |
insufficient_network_funds (409) |
POST …/banking/conversion-requests |
Not enough balance on the specific network | Wait for delivery, or lower the amount |
insufficient_network_funds_for_gas (409) |
POST …/banking/conversion-requests |
Selling a network's own coin (TRX on TRON, BTC on Bitcoin) would leave nothing to pay that network's fee; detail states the most that can be converted |
Lower the amount to at most the figure in detail |
exchange_order_failed (409) |
POST …/banking/conversion-requests |
Any other failure while placing the order, including a platform money-movement pause; detail is free text |
Retry later; do not retry in a tight loop |
Fiat accounts
code |
Operation | Condition | Remedy |
|---|---|---|---|
unsupported_currency |
POST …/banking/accounts |
Not a platform fiat currency | Use GBP, EUR or USD (see the operation) |
unsupported_provider |
POST …/banking/accounts |
A named provider the platform does not know | Omit the provider to be routed, or name a supported one |
provider_unavailable |
POST …/banking/accounts |
The named provider is not enabled for this tenant | Omit the provider |
missing_provider_prerequisites |
POST …/banking/accounts |
The customer record lacks data the provider requires (the detail names it) |
Complete the customer profile |
provider_routing_unavailable (409) |
POST …/banking/accounts |
No provider can be routed for this currency and customer right now | Retry later |
onboarding_prerequisite_not_met (409) |
POST …/banking/accounts |
A provider onboarding step has not completed | Retry after onboarding completes |
duplicate_currency_account (409) |
POST …/banking/accounts |
The customer already holds an active account in this currency at ANOTHER provider (the detail names it) |
Close the existing account first |
Beneficiaries
code |
Operation | Condition | Remedy |
|---|---|---|---|
unsupported_currency |
POST, PATCH …/banking/beneficiaries |
Not a platform fiat currency | Use an offered currency |
unsupported_provider |
POST, PATCH …/banking/beneficiaries |
A named provider that is not FIAT_REPUBLIC or OPENPAYD |
Omit it |
unsupported_payment_scheme |
POST, PATCH …/banking/beneficiaries |
A USD scheme the platform does not support | Use a listed scheme |
beneficiary_details_incomplete |
POST, PATCH …/banking/beneficiaries |
Bank details do not satisfy the scheme, or an OPENPAYD payee lacks the name attributes (the detail lists the fields) |
Supply the fields |
beneficiary_provider_validation_failed |
POST, PATCH …/banking/beneficiaries |
The banking provider pre-flight definitively rejected the details | Correct the details named |
bank_account_required (409) |
POST, PATCH …/banking/beneficiaries |
The customer has no completed fiat account in the beneficiary's currency | Open the account first |
beneficiary_creation_failed / beneficiary_update_failed (409) |
POST / PATCH …/banking/beneficiaries |
Provider-side failure; detail is the provider's message |
Retry later |
Fiat withdrawals
code |
Operation | Condition | Remedy |
|---|---|---|---|
unsupported_currency |
POST …/banking/withdrawals |
Not a platform fiat currency | Use an offered currency |
invalid_amount |
POST …/banking/withdrawals |
amount_minor is not a positive integer |
Send minor units as an integer > 0 |
beneficiary_not_found |
POST …/banking/withdrawals |
No beneficiary with that id for this customer | Check the id |
beneficiary_currency_mismatch |
POST …/banking/withdrawals |
The beneficiary's currency differs from the withdrawal currency | Use a matching beneficiary |
beneficiary_not_ready (409) |
POST …/banking/withdrawals |
The beneficiary has not finished provider provisioning | Poll the beneficiary until ready |
beneficiary_not_approved (409) |
POST …/banking/withdrawals |
The beneficiary awaits staff approval | Wait for approval (approval_status) |
source_account_not_ready (409) |
POST …/banking/withdrawals |
The customer's account in this currency is not completed | Wait for the account |
withdrawal_client_money_blocked (409) |
POST …/banking/withdrawals |
Funds are client money that cannot be withdrawn by this route | Contact support |
source_account_pot_missing (409) |
POST …/banking/withdrawals |
Ledger pot for the account is missing | Contact support |
fiat_withdrawal_prerequisites_missing (409) |
POST …/banking/withdrawals |
The customer's or the provider's LEDGER account does not exist yet (internal ledger accounts, not provider references) | Contact support |
unsupported_rail |
POST …/banking/withdrawals, POST …/payout-batches |
The rail is not valid for this currency | Use a rail from the operation's list |
rail_not_supported_by_beneficiary |
POST …/banking/withdrawals |
The beneficiary's bank details do not support that rail | Choose another rail or beneficiary |
invalid_payment_reference |
POST …/banking/withdrawals |
The reference is too long or contains disallowed characters | Shorten or clean it |
fiat_withdrawal_failed (409) |
POST …/banking/withdrawals |
Any other failure while submitting, including a platform money-movement pause; detail is free text |
Retry later |
Payout batches
code |
Operation | Condition | Remedy |
|---|---|---|---|
currency_required |
POST …/payout-batches |
No currency |
Send one |
unsupported_currency |
POST …/payout-batches |
Not a platform fiat currency (GBP, EUR, USD) — AUD included | Use an offered currency |
items_required |
POST …/payout-batches |
No items | Send at least one |
too_many_items |
POST …/payout-batches |
More than 500 items | Split the batch |
unsupported_rail |
POST …/payout-batches |
The rail is not valid for the currency | Use a listed rail |
subject_not_active |
POST …/payout-batches |
The customer is not active | Check onboarding status |
A payout batch can be refused per item while the request returns 200 — inspect each item's
status and failure_reason; see the operation.
Crypto
code |
Operation | Condition | Remedy |
|---|---|---|---|
unsupported_asset |
POST …/crypto/wallets/{asset}/address, GET …/crypto/wallets/{asset}/balance, POST …/crypto/withdrawals |
Not a supported asset (the detail lists them) |
Use a listed asset |
crypto_wallet_workflow_unavailable (409) |
POST …/crypto/wallets/{asset}/address |
Wallet opening is not configured on this host | Contact support |
invalid_amount |
POST …/crypto/withdrawals |
amount is not positive |
Send a positive amount |
whitelisted_address_required |
POST …/crypto/withdrawals |
This customer must withdraw to a whitelisted address and none was named | Whitelist the address first |
whitelisted_address_not_found |
POST …/crypto/withdrawals |
The whitelisted-address id is unknown | Check the id |
whitelisted_address_not_active (409) |
POST …/crypto/withdrawals |
The whitelisted address is not yet active (pending approval or screening) | Wait for it to become active |
whitelisted_address_asset_mismatch |
POST …/crypto/withdrawals |
The whitelisted address is for another asset or network | Use a matching address |
invalid_destination_address |
POST …/crypto/withdrawals |
The address is not valid for the asset's network | Correct the address |
crypto_withdrawal_prerequisites_missing (409) |
POST …/crypto/withdrawals |
Wallet or ledger configuration is missing | Open the wallet first; contact support if it persists |
crypto_withdrawal_workflow_unavailable (409) |
POST …/crypto/withdrawals |
Withdrawals are not configured on this host | Contact support |
crypto_withdrawal_failed (409) |
POST …/crypto/withdrawals |
Any other failure while submitting, including a platform money-movement pause or a same-key race; detail is free text |
Retry later |
The two crypto operations also pass workflow decision codes through verbatim as 422 — the
vocabulary is open and new codes ship without notice. Seen today on POST …/crypto/withdrawals:
insufficient_funds, awaiting_crypto_delivery (bought but not yet delivered to the wallet —
detail says how much you can send now), insufficient_network_funds,
ledger_available_balance_insufficient, withdrawal_fee_unavailable, asset_network_policy_unavailable,
crypto_withdrawals_disabled, crypto_withdrawal_below_minimum, provider_submission_failed.
On POST …/crypto/wallets/{asset}/address: crypto_asset_network_policy_not_configured,
crypto_deposits_disabled. A crypto withdrawal parked for review answers 202 with the normal
withdrawal body and no code — 202 means "accepted for review", not "sent".
Practical guidance
- Branch on the HTTP status first, then on
data.code(shape 1) orcode(shapes 2 and 3). Anull/missing code means "no machine reason — use the status." - Accept
application/problem+jsonas JSON. - Treat unknown codes as a refusal that will not clear by resending the same request.
- Keep the
X-Correlation-Idof every failed call in your logs. - Paths under
/api/v1/partnerthat do not appear in the reference are not supported and may answer with provider errors.