Wallet
KYC Levels

KYC Levels

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
GET end-users/:id/kyc-level is SHARED between s2s and panel. PUT end-users/:id/kyc-level is s2s-only (based on a KYC decision). A manual level change in the panel is a four-eyes request: POST end-users/:id/kyc-level/override-requests (request with wallet:enduser:kyc-override, approval by another person with wallet:enduser:kyc-approve). Raising a level requires an approved KYC session on both surfaces. GET end-users/:id/kyc-level/history is panel-only.

Every end user carries a KYC level. The level determines two things: which operations the user may perform, and how high their limits are.

Reading the level

GET /api/v1/wallet/end-users/{endUserId}/kyc-level

On the end-user surface:

GET /api/portal/v1/wallet/me/kyc-level

The response carries the current level and which operations are open at it. In your app, do not hide locked capabilities — show them locked; users will not upgrade without seeing what they gain.

Updating the level

A level is raised only on the basis of a verifiable KYC decision. The source of that decision is an approved KYC session in the same tenant; the wallet does not raise a level without confirming that session with the KYC service.

PUT /api/v1/wallet/end-users/{endUserId}/kyc-level

This call requires an Idempotency-Key header and the wallet:enduser:kyc-override permission on your key.

FieldTypeRequiredDescription
kycLevelstring✅New level: NONE, LOW, SUBSTANTIAL, HIGH
sourceRefstring✅On upgrade: the id (sessionId) of the approved KYC session. On downgrade: a reference to the record the decision is based on
externalCustomerIdstring—Target the user by your own id
programIdstring—Required across multiple programs
reasonstring—Audit reason
effectiveAtstring—Effective time
expectedCurrentLevelstring—Optimistic lock: rejected if the current level differs

When an upgrade is accepted

If the target level is above NONE and not lower than the current level (an upgrade, or a renewal of the same level), all of the following must hold:

  1. sourceRef is the id of a KYC session in your tenant.
  2. The session is approved and its latest decision is an approval.
  3. The session belongs to this end user: the session's externalCustomerId matches the end user's externalCustomerId, or the end user's kycRef matches the session's subject key.
  4. The same session has not already been used to set the level of another end user.
  5. The level the session attests is equal to or higher than the requested level.

The attested level is the weaker of the identity-evidence strength and the person-to-document binding measured in the approved session. A session with HIGH identity evidence and SUBSTANTIAL binding, for example, unlocks at most SUBSTANTIAL.

If any condition fails, the request is rejected with WALLET_KYC_LEVEL_UNVERIFIED (HTTP 403) and the level is unchanged. If the KYC service cannot be reached, the request is rejected with WALLET_KYC_VERIFIER_UNAVAILABLE (HTTP 503); an upgrade that cannot be verified is never applied.

⚠️

A free-text reference (e.g. REVIEW-2026-07-21-00042) is not accepted to raise a level. This applies to both the API and the panel: the panel's four-eyes approval is an additional requirement, not a substitute for the KYC decision.

Downgrading

Lowering a level (e.g. HIGH → LOW or NONE) does not require a KYC decision. sourceRef is still required; enter the record the decision is based on (review number, compliance decision).

Level at end-user creation

End users are always created at NONE. If the creation request carries a kycLevelCache value other than NONE, it is rejected with WALLET_KYC_LEVEL_UNVERIFIED. Raise the level after creation using the endpoint above.

Use expectedCurrentLevel when concurrent updates are possible. If two systems change the level at once, the request that does not carry the expected level is rejected instead of silently overwriting.

Unverified level (NONE)

The following rules for an unverified wallet apply at platform level to every program, including programs with no settings of their own.

OperationAt NONE
Top-up (card, agent)Open, up to the balance cap
Paying with the wallet (payment to a merchant)Open
Wallet-to-wallet transfer (W2W_TRANSFER)Closed
Withdrawal to IBAN and bank account (W2IBAN_WITHDRAW, WITHDRAW_BANK)Closed
Cash-out — ATM and QR (CASHOUT_ATM, CASHOUT_QR)Closed
Card issuing and card spending (CARD_AUTH)Closed

Closed operations require at least LOW and are rejected with WALLET_KYC_LEVEL_INSUFFICIENT (HTTP 403). Authorizations on a card previously issued to an unverified user are declined as well.

Balance cap

The balance of a TRY account at NONE cannot exceed ₺5,000. A card top-up, agent top-up or incoming wallet transfer that would exceed the cap is rejected with WALLET_KYC_BALANCE_CAP_EXCEEDED (HTTP 403) before any money moves. The cap is lifted once the user's level is raised.

The balance cap applies only to open-loop programs. Closed-loop programs (where the balance can be spent only within the program's own network) have no balance cap; the operation restrictions and the transaction ceilings of the limit scheme still apply.

Level → capability matrix

The platform rules are a floor. Your program can only tighten it, never relax it: with railPolicy.minKycLevelByOperation you can require a higher level for an operation (e.g. SUBSTANTIAL for wallet-to-wallet transfers); a setting that tries to open a floor operation at NONE has no effect. The threshold applied is the higher of the platform floor and your program setting.

For operations outside the floor (e.g. payments) the threshold comes only from your program setting; without one, no level check runs for that operation.

A typical setup:

LevelTypical entitlements
NONETop-up and payments; ₺5,000 balance cap in open loop
LOWTransfers, withdrawals, cash and card — with low ceilings
SUBSTANTIAL / HIGHThe same operations, with high ceilings

Which amount ceilings apply at each level is decided by your program's limit scheme. See Limits.

Before taking a program live, walk this checklist:

  • Are thresholds set for operations you want stricter than the platform floor?
  • Are ceilings filled in for every level in the limit scheme?
  • Does your verification flow store the id of the approved KYC session? You will need it to raise the level.

The upgrade flow

A typical product flow:

  1. The user hits a limit or a locked capability while attempting an operation.
  2. Your app says "this needs a higher level" and starts a KYC session. When creating the session, set externalCustomerId to the end user's externalCustomerId in the wallet.
  3. When the session is approved, your system calls PUT /wallet/end-users/{id}/kyc-level with the session id in sourceRef.
  4. The wallet confirms the decision with the KYC service; the user's limits and entitlements update immediately.

At step 2, show concretely what the new level unlocks ("withdraw up to ₺50,000 per day"). An abstract "verify your account" prompt is the lowest-converting screen in this flow.

Related products

The verification process itself (documents, liveness, address) is a separate product and runs independently of the wallet. The wallet consumes only the outcome — the level. See Identity.

Common mistakes

SymptomCauseFix
WALLET_KYC_LEVEL_UNVERIFIEDsourceRef is not an approved KYC session, belongs to another person, or does not attest the requested levelSend the correct session id; check the session's externalCustomerId
WALLET_KYC_LEVEL_UNVERIFIED (at creation)The creation request carries a kycLevelCache other than NONECreate the user at NONE, then raise the level
WALLET_KYC_LEVEL_INSUFFICIENTAn unverified user attempted a transfer, withdrawal, cash-out or card operationSend the user to the verification flow
WALLET_KYC_BALANCE_CAP_EXCEEDEDThe top-up exceeds the unverified balance capLower the amount or raise the level
Level raised but limits unchangedNo tier for that level in the limit schemeCheck the scheme
Upgrade rejected with WALLET_KYC_LEVEL_CONFLICTexpectedCurrentLevel mismatchRe-read the current level and retry
Users do not understand rejectionsThe UI hides locked capabilitiesShow them locked, with their requirement