Error Handling
Wallet errors are designed to answer three questions: what happened, did money move, and can this be retried.
Error classes
| Class | HTTP | Retryable | Example |
|---|---|---|---|
| Authentication | 401 / 403 | After fixing | INVALID_SIGNATURE, NONCE_REPLAY |
| Validation | 400 | After fixing the body | Missing field, invalid currency |
| Business rule | 400 / 409 | If conditions change | Insufficient balance, limit exhausted |
| Gate rejection | 403 | If conditions change | KYC level too low, capability disabled |
| Integration / external | 502 / 504 | Carefully | Bank integration unresponsive |
| Server | 500 | With the same idempotency key | Unexpected 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.
| Situation | What to do |
|---|---|
| Network error / timeout | Retry with the same key; you get the same response back |
500 | Retry with the same key |
AWAITING_RECON | Do not retry; wait for reconciliation |
HELD | Do not retry; wait for the review outcome |
INDETERMINATE | Do not retry with a new key; query the operation or repeat with the same key — it settles shortly |
complete failed | Do 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:
| Field | Content |
|---|---|
failureCode | Machine-readable code |
failureReason | Human-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 state | What to tell the user |
|---|---|
| Insufficient balance | "Insufficient balance" plus the amount needed (requiredAmount) |
| Limit exhausted | Which limit and when it resets |
| KYC too low | Which 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)
| Code | Meaning |
|---|---|
WALLET_PROGRAM_ARCHIVE_BLOCKED_BALANCE | Accounts still hold a balance — settle the balances |
WALLET_PROGRAM_ARCHIVE_BLOCKED_IN_FLIGHT | Money is in flight — close the queue |
WALLET_PROGRAM_ARCHIVE_UNVERIFIABLE | Balances 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).
| Code | HTTP | Meaning | What to do |
|---|---|---|---|
PRODUCT_NOT_ENABLED | 403 | The identity is not entitled to the wallet product | Check your subscription and the API key's product list |
PRODUCT_ENTITLEMENT_UNAVAILABLE | 503 | Entitlement 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:
- Your organisation needs an active subscription to the wallet product
- The API key you call with must list
WALLET_SERVICEin 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.
| Code | HTTP | Meaning | What to do |
|---|---|---|---|
WALLET_INSUFFICIENT_SCOPE | 403 | The key lacks the wallet permission for this call | Grant 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
| Symptom | Cause | Fix |
|---|---|---|
| Duplicates on retry | The key changed on each attempt | Derive the key from business meaning |
| Every request returns 401 | Server clock drift | Sync with NTP |
Some endpoints return 403 (WALLET_INSUFFICIENT_SCOPE) | The key lacks the permission for those calls | Add the permission → Key permission |
Every endpoint returns 403 (PRODUCT_NOT_ENABLED) | The identity is not entitled to the wallet product | Subscription + the key's product list → Product entitlement |
| Messages arrive in English | Accept-Language not sent | Add the header |