KYC Levels
- 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-levelis SHARED between s2s and panel.PUT end-users/:id/kyc-levelis 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 withwallet:enduser:kyc-override, approval by another person withwallet:enduser:kyc-approve). Raising a level requires an approved KYC session on both surfaces.GET end-users/:id/kyc-level/historyis 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-levelOn the end-user surface:
GET /api/portal/v1/wallet/me/kyc-levelThe 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-levelThis call requires an Idempotency-Key header and the wallet:enduser:kyc-override permission on your key.
| Field | Type | Required | Description |
|---|---|---|---|
kycLevel | string | ✅ | New level: NONE, LOW, SUBSTANTIAL, HIGH |
sourceRef | string | ✅ | On upgrade: the id (sessionId) of the approved KYC session. On downgrade: a reference to the record the decision is based on |
externalCustomerId | string | — | Target the user by your own id |
programId | string | — | Required across multiple programs |
reason | string | — | Audit reason |
effectiveAt | string | — | Effective time |
expectedCurrentLevel | string | — | 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:
sourceRefis the id of a KYC session in your tenant.- The session is approved and its latest decision is an approval.
- The session belongs to this end user: the session's
externalCustomerIdmatches the end user'sexternalCustomerId, or the end user'skycRefmatches the session's subject key. - The same session has not already been used to set the level of another end user.
- 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.
| Operation | At 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:
| Level | Typical entitlements |
|---|---|
NONE | Top-up and payments; ₺5,000 balance cap in open loop |
LOW | Transfers, withdrawals, cash and card — with low ceilings |
SUBSTANTIAL / HIGH | The 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:
- The user hits a limit or a locked capability while attempting an operation.
- Your app says "this needs a higher level" and starts a KYC session. When creating the session, set
externalCustomerIdto the end user'sexternalCustomerIdin the wallet. - When the session is approved, your system calls
PUT /wallet/end-users/{id}/kyc-levelwith the session id insourceRef. - 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
| Symptom | Cause | Fix |
|---|---|---|
WALLET_KYC_LEVEL_UNVERIFIED | sourceRef is not an approved KYC session, belongs to another person, or does not attest the requested level | Send the correct session id; check the session's externalCustomerId |
WALLET_KYC_LEVEL_UNVERIFIED (at creation) | The creation request carries a kycLevelCache other than NONE | Create the user at NONE, then raise the level |
WALLET_KYC_LEVEL_INSUFFICIENT | An unverified user attempted a transfer, withdrawal, cash-out or card operation | Send the user to the verification flow |
WALLET_KYC_BALANCE_CAP_EXCEEDED | The top-up exceeds the unverified balance cap | Lower the amount or raise the level |
| Level raised but limits unchanged | No tier for that level in the limit scheme | Check the scheme |
Upgrade rejected with WALLET_KYC_LEVEL_CONFLICT | expectedCurrentLevel mismatch | Re-read the current level and retry |
| Users do not understand rejections | The UI hides locked capabilities | Show them locked, with their requirement |