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 |
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_invalidIdempotency-Keyconflict →409 idempotency_key_reused/idempotency_key_in_progress- 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 (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
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 same key was already used by this API key for a different method, path or body | 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 |
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 | Retry with backoff; a mutating retry with the same Idempotency-Key re-executes (a failed attempt does not lock the key) — check state first |
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
Idempotency entries are retained for 24 hours. The shared replay layer hashes known-length JSON request bodies up to 1 MiB (1,048,576 bytes). A chunked request with no known content length, or a request above that size, currently proceeds without middleware replay protection. A successful response above the same 1 MiB boundary is not stored for byte-for-byte replay. A first attempt that returns any non-2xx does not lock the key. 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 |
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.