Wallet
Limits

Limits

Who calls these endpoints?External API (s2s)
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/limits on portal). Writing limit schemes is panel-only: limit-schemes, programs/:id/limit-scheme, programs/:id/shared-limit-caps, programs/:id/otp-policy — permission wallet: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:

RowCounter
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.
BothBoth 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:

FieldTypeDescription
kycLevelstringWhich KYC level it applies to
operationTypestring* (or empty) = wallet total; a type name = that type's own counter
currencystringSelector — which operations the row matches. * = every currency
amountCurrencystringDenomination — what the amounts below are expressed in. * is invalid
singleMaxstringMaximum per transaction (minor-unit)
dailyAmountstringDaily total amount ceiling
monthlyAmountstringMonthly total amount ceiling
dailyCountnumberDaily transaction count ceiling
monthlyCountnumberMonthly 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:

  • currency is a selector: "which operations does this row apply to". * is a legitimate value for it.
  • amountCurrency is 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}/limits

You can narrow with operationType and currency. Each row in the response states which counter it shows in the bucket field:

bucketMeaning
OPERATION_TYPEThe own counter of a type-specific row
WALLET_TOTAL_OUTFLOWThe outflow total of the * row
WALLET_TOTAL_INFLOWThe 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/limits

Show 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:

SituationBehaviour
Limit check unavailable (outbound money)The operation is rejected — there is no "let it through"
Card authorization, limit service slowIf no answer arrives within a short deadline, the authorization is declined
Transfer, withdrawal, cash-out, card — unverified userRejected 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 configuredThe 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

SymptomCauseFix
"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 directionCheck the bucket: WALLET_TOTAL_OUTFLOW row
Limit not restoredThe 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 rejectedrequiredAmount (incl. fee + tax) exceeds itCompare against requiredAmount
Hitting the ceiling unexpectedly earlyIn the platform template all outflow types share one totalFor 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_EXCEEDEDIn an open-loop program, the unverified account's balance would exceed the capRaise the user's level with a verified KYC decision