Wallet
Error Handling

Error Handling

Wallet errors are designed to answer three questions: what happened, did money move, and can this be retried.

Error classes

ClassHTTPRetryableExample
Authentication401 / 403After fixingINVALID_SIGNATURE, NONCE_REPLAY
Validation400After fixing the bodyMissing field, invalid currency
Business rule400 / 409If conditions changeInsufficient balance, limit exhausted
Gate rejection403If conditions changeKYC level too low, capability disabled
Integration / external502 / 504CarefullyBank integration unresponsive
Server500With the same idempotency keyUnexpected error

Authentication errors

The s2s signing surface's codes and fixes live on the Authentication page: AUTH_HEADERS_MISSING, INVALID_DATE, NONCE_REPLAY, INVALID_KID, MERCHANT_SUSPENDED, IP_NOT_ALLOWED, DIGEST_MISSING, INVALID_DIGEST, INVALID_SIGNATURE. Two further 403 refusals come after the signature: PRODUCT_NOT_ENABLED and WALLET_INSUFFICIENT_SCOPE.

What fail-closed means

On a money path, the wallet rejects uncertainty:

⚠️

If a gate (limit, KYC, risk, provider) cannot answer, the operation is rejected. No money movement has a "couldn't check, let it through" path.

In practice:

  • If the limit service is unresponsive, outbound operations are rejected.
  • On card authorization, if the limit answer does not arrive within a short deadline, the authorization is declined.
  • Integrations with no connected provider are closed by program configuration; calling a closed integration is rejected (Roadmap).

The one exception is operation types outside the platform floor with no KYC threshold configured — that gate does not engage. Transfers, withdrawals, cash-out and card operations are in the platform floor and are rejected for an unverified user in every program. See KYC Levels.

Retry rules

⚠️

Retry with the same Idempotency-Key. Retrying with a new key creates a second money movement if your first request actually succeeded.

SituationWhat to do
Network error / timeoutRetry with the same key; you get the same response back
500Retry with the same key
AWAITING_RECONDo not retry; wait for reconciliation
HELDDo not retry; wait for the review outcome
INDETERMINATEDo not retry with a new key; query the operation or repeat with the same key — it settles shortly
complete failedDo not mint a new nonce — query the operation

Uncertain outcome: INDETERMINATE

When the outcome of a money movement cannot be settled at that moment (for example, it is not known whether the ledger entry was confirmed), the wallet reports it not as an error but as status: "INDETERMINATE". It means: money may have moved, and the final state has not been recorded yet. The wallet settles the operation on its own (COMPLETED, FAILED or REVERSED).

⚠️

INDETERMINATE is not a rejection. Responding to it with compensation (charging again, paying with another method, refunding to a card) can create a double money movement. The right behaviour is to wait and query.

Endpoints that return this outcome: wallet payment approval (Wallet Payments) and agent top-up (Top-up).

Error fields on the operation

A failed operation carries the failure in two fields:

FieldContent
failureCodeMachine-readable code
failureReasonHuman-readable explanation

Do not surface failureReason verbatim to end users; translate it into your product language. Show failureCode to your support team.

Language

Error messages follow the Accept-Language header (Turkish by default).

-H "Accept-Language: en"

Carry the user's language with this header in your end-user app; the language of OTP messages is set separately by the locale field in the request body. They are two different settings.

Presenting errors to users

Technical stateWhat to tell the user
Insufficient balance"Insufficient balance" plus the amount needed (requiredAmount)
Limit exhaustedWhich limit and when it resets
KYC too lowWhich level is needed and how to upgrade
HELD"Your transaction is under review" — do not say failed
AWAITING_RECON"Your transaction is being verified" — do not say failed
INDETERMINATE"Your transaction is being verified" — do not say failed, do not offer another payment method
Integration error"Could not complete right now, please try again"
⚠️

Calling an uncertain state "failed" makes users retry and causes double processing. It is the most expensive UI mistake in wallet integrations.

Errors that carry a structured details

Some errors are not plain text: they carry a machine-readable breakdown under details. Do not parse them out of the message string.

Program archiving (409)

CodeMeaning
WALLET_PROGRAM_ARCHIVE_BLOCKED_BALANCEAccounts still hold a balance — settle the balances
WALLET_PROGRAM_ARCHIVE_BLOCKED_IN_FLIGHTMoney is in flight — close the queue
WALLET_PROGRAM_ARCHIVE_UNVERIFIABLEBalances could not be verified inline; the platform procedure is required

The ..._IN_FLIGHT error carries details.inFlightByAxis (axis → count), details.capped, details.probeCap and a per-axis details.axes[]. Four axes: OPERATION, CASHOUT_CODE, CARD_AUTHORIZATION, BULK_PAYOUT_BATCH.

⚠️

Counting is capped at probeCap (500). An axis with capped: true must be rendered as "500+"; the capping flag is per axis (axes[].capped). The first two codes need different user-facing messages — "settle the balances" and "close the queue" are not the same job. Details: Panel — Programs.

Bulk import / offboard (422)

WALLET_BULK_IMPORT_REJECTED and WALLET_BULK_OFFBOARD_REJECTED carry the per-row report under details. No row was written. See Corporate Accounts.

Limit denomination mismatch

WALLET_LIMIT_CURRENCY_MISMATCH — the limit row's amount caps are denominated in a different currency than the operation. No FX conversion is performed; because the cap cannot be compared honestly, the operation is rejected. The fix is the operator's: add a limit row for that currency. See Limits.

Product entitlement (PRODUCT_NOT_ENABLED)

The Wallet API is a product-gated surface: even with a valid signature, no endpoint opens — read endpoints included — unless your identity is entitled to the wallet product (WALLET_SERVICE).

CodeHTTPMeaningWhat to do
PRODUCT_NOT_ENABLED403The identity is not entitled to the wallet productCheck your subscription and the API key's product list
PRODUCT_ENTITLEMENT_UNAVAILABLE503Entitlement could not be verified (source unreachable)Retry with exponential backoff
⚠️

Do not confuse the two. PRODUCT_NOT_ENABLED is a permanent refusal: retrying without changing anything on your account returns the same result. PRODUCT_ENTITLEMENT_UNAVAILABLE is temporary and does not say "you lack the product" — it says "we cannot verify it right now". The gate is fail-closed, so no operation passes on an entitlement that could not be verified.

If you get PRODUCT_NOT_ENABLED, both conditions must hold:

  1. Your organisation needs an active subscription to the wallet product
  2. The API key you call with must list WALLET_SERVICE in its products

If you have the subscription and still get this error, the second condition is almost always the missing one: an API key is a deliberate narrowing, and subscribing does not widen your existing keys automatically. Details: Error handling.

A change made in the panel is not reflected instantly: the entitlement snapshot is embedded in your access token and refreshes when the token does.

Key permission (WALLET_INSUFFICIENT_SCOPE)

Beyond the product, every wallet call requires a specific permission on the API key — for example wallet:operation:intervene to move money inside the wallet, or wallet:bank-payout:operate to send money to an IBAN or a bank account. The check is fail-closed: a key without the permission for the call is refused, and nothing is executed.

CodeHTTPMeaningWhat to do
WALLET_INSUFFICIENT_SCOPE403The key lacks the wallet permission for this callGrant the permission listed on the Authentication page to the key, then retry
{
  "status": "failure",
  "code": "WALLET_INSUFFICIENT_SCOPE",
  "message": "…",
  "statusCode": 403,
  "requestId": "…"
}

Unlike PRODUCT_NOT_ENABLED, which blocks every wallet endpoint, this error affects only the calls the key is not permitted for — the same key may still read balances while a transfer is refused. Retrying without changing the key's permissions returns the same result.

Common mistakes

SymptomCauseFix
Duplicates on retryThe key changed on each attemptDerive the key from business meaning
Every request returns 401Server clock driftSync with NTP
Some endpoints return 403 (WALLET_INSUFFICIENT_SCOPE)The key lacks the permission for those callsAdd the permission → Key permission
Every endpoint returns 403 (PRODUCT_NOT_ENABLED)The identity is not entitled to the wallet productSubscription + the key's product list → Product entitlement
Messages arrive in EnglishAccept-Language not sentAdd the header