Wallet
Overview

Wallet

Wallet lets you run program-scoped accounts under your own brand: your customers top up, send money to each other, pay you, pay by QR, and earn points and cashback. Balances live on a double-entry ledger; every movement produces an operation record and an audit trail.

All amounts are minor-unit integers carried as strings: "amount": "10050" means ₺100.50. There are no decimal or float amounts anywhere in the Wallet API.

Three surfaces

The same capability is exposed on three surfaces with different authority scopes. They are separate controllers — one endpoint is never shared across three identities.

SurfaceCallerPathAuthenticationEndpoints
External API (s2s)Your backend/api/v1/wallet/*Every request is Ed25519-signed — the signature is the identity; it is not exchanged for a Bearer60
Portal / SDKThe end user's mobile/web app/api/portal/v1/wallet/*Publishable key → OTP → customer session (JWT)87
Merchant panelYour operations team/api/panel/v1/wallet/*Panel session (JWT) + RBAC + four-eyes186

Example: "list operations" exists on all three, but s2s returns the tenant's operations, the panel returns all of them (PII masked), and the portal returns only the session owner's own operations.

⚠️

The surfaces are not interchangeable: 139 panel endpoints have no s2s counterpart, so you cannot build your own merchant panel with a signing key. Measured comparison: surface comparison matrix.

What you can build

CapabilityDescriptionStatus
Program & brandingYour own wallet program: currencies, branding, fee and limit policy✅
End users & accountsIndividual (PERSONAL) and business (BUSINESS) users, multi-currency accounts✅
Card top-upTop up by card — delegated to the payment stack, 3D Secure included✅
Agent top-upAn agent takes cash and credits the customer from a float account✅
Wallet-to-wallet transferSend by phone number or wallet number, OTP-confirmed✅
Wallet paymentsCustomer → business payment, with commission and cashier reference✅
QRGenerate, parse, collect and pay by QR✅ (closed loop)
LoyaltyCashback campaigns, points, tiers and badges✅
Limits & feesKYC-tiered limit buckets, fee schedules and quotes✅
Refund to walletCredit a card refund to the wallet instead of the card network✅
Bank transfer top-upPer-user deposit instructions, matched funds credited automatically🚧
Bank withdrawal & IBAN transferWithdraw to the user's own IBAN, transfer to a third-party IBAN🚧
Bulk payoutsMany payouts from a list, with maker-checker approval🚧
Invoices & bill payIn-wallet invoicing; biller inquiry and payment🚧
ATM / QR cash-outCardless cash withdrawal code🚧
Card issuingVirtual/physical card bound to the wallet🚧

For 🚧 capabilities the API contract is complete and stable; running them in production requires a connected provider (bank integration, biller, ATM network, card issuer). See the Roadmap.

Operation lifecycle

Every call that moves money produces an operation. These are the exact status values you see in GET /wallet/operations/:id and in webhook payloads:

Wallet operation lifecycleState machine: an operation opens as INITIALIZED, becomes CHALLENGED when verification is required, moves to POSTING after confirmation and, in a closed loop, goes straight to COMPLETED. Movements that leave over a bank integration wait in BANK_PROCESSING first; they become COMPLETED once reconciled, or AWAITING_RECON when the outcome stays uncertain — neither is a failure.VERIFYCONFIRMCLOSED LOOPTO THE BANKRECONCILEDUNCERTAININITIALIZEDCHALLENGEDPOSTINGCOMPLETEDBANK_PROCESSINGAWAITING_RECONLEGENDIN PROGRESSWAITING EXTERNALLYRECONCILING — NOT A FAILUREEND
The happy path plus the two waiting states that get mistaken for failure. Terminal states (FAILED, REVERSED, EXPIRED) and the risk hold (HELD) are left out — they are all in the table below. The diagram makes one point: BANK_PROCESSING and AWAITING_RECON are the middle of the road, not the end of it.
StatusMeaning
INITIALIZEDOperation opened, funds reserved, awaiting confirmation
CHALLENGEDWaiting for OTP / step-up verification
POSTINGLedger entries being written
COMPLETEDDone — reflected in the balance
FAILEDFinal failure; reserved funds released
REVERSEDA completed operation was reversed (refund/undo)
REVERSINGA reversal has started and awaits the ledger result; the system completes it automatically
BANK_PROCESSINGIn flight on the bank side
AWAITING_RECONOutcome uncertain — sent to reconciliation instead of being silently failed
HELDHeld by a risk decision; funds blocked and unusable
EXPIREDTimed out before confirmation; reservation released
⚠️

AWAITING_RECON, HELD and REVERSING are not terminal. Do not treat them as failures and retry — query with the same Idempotency-Key or wait for the webhook.

The money-movement pattern: quote → execute → complete

Most outbound operations take three steps, so the user sees the fee before the debit and the confirmation is verifiable:

  1. /quote — returns fee, tax, limit eligibility and (where relevant) which integration will be used. No money moves.
  2. /… (execute) — opens the operation, reserves funds, triggers OTP if required. Returns integrityHash + nonce.
  3. /…/:id/complete — confirm with the integrityHash and nonce from step 2, plus otpCode when an OTP was issued.

integrityHash guarantees the amount/fee shown in step 2 is the same as the one settled in step 3. If what the user saw differs from what would leave the account, the request is rejected.

Security model

  • Moving money means signing. On the s2s surface, every request — reads included — is signed with your Ed25519 key, and each call needs the matching wallet permission on that key (Key permissions).
  • Idempotency is mandatory. Money-moving and record-creating calls require an Idempotency-Key (full list). Replaying the same key returns the original response instead of repeating the operation.
  • Four eyes. For manual intervention, campaign approval and bulk payout approval, the requester and the approver cannot be the same principal — enforced in the service layer by principal comparison, not by a role gate. A manual intervention is a stored request: the requester and the approver are taken from their panel sessions, never from the request body.
  • Fail closed. If a gate (limit, KYC, fraud, provider) cannot answer, the operation is rejected. There is no "let it through" path.

Related products