Limits
- Base path
/api/v1/wallet- Identity
- Ed25519 request signature —
X-Key-Id,X-Date,X-Nonce,Digest(when there is a body),X-Signature. ±5 min clock window, single-use nonce, optional per-key IP allow-list. - Authorization
- The key must carry the wallet product and the wallet permission the call requires; a call without it gets
403 WALLET_INSUFFICIENT_SCOPE. Permissions per endpoint: Key permissions. - Cross-surface
- Reading limits exists on all three surfaces (
GET end-users/:id/limits,me/limitson portal). Writing limit schemes is panel-only:limit-schemes,programs/:id/limit-scheme,programs/:id/shared-limit-caps,programs/:id/otp-policy— permissionwallet:limit:manage.
The limit engine decides how much and how often a user can transact. Every money movement passes the limit gate before it starts; if the gate refuses, the operation is never opened.
Wallet total and operation-type counters
Two kinds of rows in your scheme can bind an operation. The row's operationType decides which counter is used:
| Row | Counter |
|---|---|
operationType: "*" | Wallet total. All operation types in the same direction share one counter. |
A type such as operationType: "W2W_TRANSFER" | That type's own counter only. |
| Both | Both apply: the operation must fit in its own type counter and in the wallet total. |
The wallet total is two separate counters by direction:
- Outflow: wallet-to-wallet transfer, IBAN transfer, bank withdrawal, payment, bill payment, cash-out, card spend, bulk payout.
- Inflow: card top-up, bank-transfer top-up, agent top-up, refund credit.
Money you top up does not consume the spending counter; the same amount is never counted twice.
Every row of the platform template carries operationType: "*". If you have not assigned your own scheme, a user's daily and monthly ceiling is filled jointly by all outflow types: for example, an unverified user can make transfers, payments and bill payments in the same month only up to the combined monthly ceiling; there is no separate ceiling per type.
Example: if your scheme has a * row with a monthly 500,000 and a W2W_TRANSFER row with a monthly 300,000, the user can send at most 300,000 wallet-to-wallet and all outflows together cannot exceed 500,000. A user who has sent 300,000 wallet-to-wallet can use at most 200,000 for payments in the same month.
Limit schemes
A limit scheme defines ceilings at the intersection of KYC level × operation type × currency. Schemes are assigned to programs and users.
A tier carries:
| Field | Type | Description |
|---|---|---|
kycLevel | string | Which KYC level it applies to |
operationType | string | * (or empty) = wallet total; a type name = that type's own counter |
currency | string | Selector — which operations the row matches. * = every currency |
amountCurrency | string | Denomination — what the amounts below are expressed in. * is invalid |
singleMax | string | Maximum per transaction (minor-unit) |
dailyAmount | string | Daily total amount ceiling |
monthlyAmount | string | Monthly total amount ceiling |
dailyCount | number | Daily transaction count ceiling |
monthlyCount | number | Monthly transaction count ceiling |
currency and amountCurrency are not the same thing
These two fields are two different axes, and confusing them was a measured production defect:
currencyis a selector: "which operations does this row apply to".*is a legitimate value for it.amountCurrencyis a denomination: "what are these amounts expressed in". For an amount,*is meaningless — 10,000 of what?
What actually happened: rows carrying currency='*' had TRY amounts, so in a multi-currency program a USD operation was measured against a $2,750 ceiling instead of a ₺2,750 one — the real risk boundary was applied roughly 35× wider.
A ceiling in a non-matching currency is fail-closed. If the row's amountCurrency differs from the operation currency, the operation is rejected with WALLET_LIMIT_CURRENCY_MISMATCH. No FX conversion is performed: the limit engine has no rate source, and pushing a ceiling through a floating rate would make it unpredictable.
The fix is the operator's and it is a single row: add a tier for that currency with currency=<CCY> and amountCurrency=<CCY>.
Count ceilings are unaffected. "10 transactions per day" is currency-independent; rows carrying only dailyCount/monthlyCount leave amountCurrency empty and this gate stays silent. A row carrying an amount, however, must carry a denomination (a database constraint enforces it too).
The same contract applies to cross-channel shared cap tiers.
Schemes are managed from the merchant panel: Panel — Limit & Fee Schemes.
Reading a user's limit status
GET /api/v1/wallet/end-users/{endUserId}/limitsYou can narrow with operationType and currency. Each row in the response states which counter it shows in the bucket field:
bucket | Meaning |
|---|---|
OPERATION_TYPE | The own counter of a type-specific row |
WALLET_TOTAL_OUTFLOW | The outflow total of the * row |
WALLET_TOTAL_INFLOW | The inflow total of the * row |
When you pass operationType, every counter binding that type is returned: the type-specific row and, if present, the wallet total. The operation passes only if all of them have room.
The same information on the end-user side:
GET /api/portal/v1/wallet/me/limitsShow the remaining limit as soon as the amount is typed. Receiving a limit rejection after pressing "Send" is the most complained-about behaviour in wallet UX.
Reservations and operation lifetime
The counter is reserved when the operation starts and committed when it completes:
quote → check (no reservation)
execute → reservation (counter decrements)
complete → commit
fail/expire → reservation released (counter restored)The reservation is held until the operation reaches an outcome:
- Its duration is derived from the operation's normal lifetime (approval window, bank withdrawal reconciliation time, cash-out code validity, card authorization hold, and so on).
- If the operation is still open when that duration elapses (for example it is waiting in fraud review, or the bank result has not arrived), the reservation is not released; it is extended.
- When the operation fails or is cancelled, the reservation is released and the counter is restored.
When an operation waiting in review is approved, the limit is checked again. If the reservation was released in the meantime and the user has since used up their limit with other operations, the approval is rejected and the operation stays on hold.
Committed and in-progress spending is recorded durably. Even if the counter service restarts, the limit a user has used that day and month is not reset, and a committed spend is never dropped from the counter.
On partial capture (for example capturing part of an authorized amount) the counter charges the captured amount; the difference is returned to the amount counter. The operation counts once toward the count ceiling.
Cross-channel shared cap
On top of the scheme rows, an umbrella cap shared by selected outbound channels can be defined per program. When enabled, IBAN transfers, bank withdrawals, cash-out and card authorizations consume the same daily/monthly pool, while the scheme-row counters remain in force as sub-limits.
The shared cap is off by default; the platform template carries no value for it. Unless it is enabled for your program, only the scheme-row counters apply. Enabling it is done per program in the panel with thresholds your compliance team defines.
Fail-closed behaviour
What happens when the limit service cannot answer is defined per operation:
| Situation | Behaviour |
|---|---|
| Limit check unavailable (outbound money) | The operation is rejected — there is no "let it through" |
| Card authorization, limit service slow | If no answer arrives within a short deadline, the authorization is declined |
| Transfer, withdrawal, cash-out, card — unverified user | Rejected regardless of program settings (platform floor) |
No limit row matches the operation (neither type-specific nor *) | The operation is rejected with WALLET_LIMIT_TIER_UNDEFINED |
| No KYC threshold configured | The gate does not engage — programs without a threshold are unaffected |
Wallet-to-wallet transfers, IBAN and bank withdrawals, cash-out and card operations require at least LOW at platform level; your program can only raise that threshold. For an operation outside the floor, if you do not configure a KYC threshold, no level check runs for it. See KYC Levels.
Unverified balance cap
The balance of a TRY account at NONE cannot exceed ₺5,000. The cap is a check separate from the transaction counters: a card top-up, agent top-up or incoming wallet transfer is rejected with WALLET_KYC_BALANCE_CAP_EXCEEDED before any money moves if the account's current balance plus the amount would exceed it. There is no balance cap at LOW and above; transaction ceilings come from the limit scheme.
The balance cap applies only to open-loop programs. Closed-loop programs have no balance cap; you set the transaction ceilings of unverified users through the limit scheme.
Explaining a limit rejection to the user
limitBlocked: true in the quote response tells you about the rejection in advance. Show the user:
- which bucket is exhausted (daily or monthly, amount or count),
- when it resets,
- whether a higher KYC level raises the ceiling.
See KYC Levels.
Common mistakes
| Symptom | Cause | Fix |
|---|---|---|
| "I have limit left for this type but I'm rejected" | The wallet total is full: the * row is shared by all types in the same direction | Check the bucket: WALLET_TOTAL_OUTFLOW row |
| Limit not restored | The operation is still open (in review, awaiting the bank result) | The reservation is released once the operation reaches an outcome |
| Amount below the ceiling but rejected | requiredAmount (incl. fee + tax) exceeds it | Compare against requiredAmount |
| Hitting the ceiling unexpectedly early | In the platform template all outflow types share one total | For separate ceilings per type, define type-specific rows in your own scheme; the * row still caps the total |
Top-up rejected with WALLET_KYC_BALANCE_CAP_EXCEEDED | In an open-loop program, the unverified account's balance would exceed the cap | Raise the user's level with a verified KYC decision |