Wallet
Wallet Payments

Wallet Payments

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
These endpoints are on the s2s surface (payments/buy, collect, payments/:id/approve, payments/:id). End-user approval lives on the portal side (me/payments/:requestId/approve, me/authorizations/*) and requires step-up.

A wallet payment is an individual user paying a business account inside the program from their balance. No card network is involved; money moves inside the tenant, which makes it instant and low-cost.

There are two collection models:

ModelWho starts itWhen to use
Payment request (payments/buy)The business, targeting the buyer by phone or wallet numberE-commerce, remote sales, invoice collection
Code collection (CPM) (collect)The business, using a single-use code the customer showsPhysical checkout, POS, in-store

Payment request

Create the request

POST /api/v1/wallet/payments/buy
FieldTypeRequiredDescription
buyerWalletNostring⚠️Buyer's wallet number
buyerPhonestring⚠️Buyer's phone
amountstring✅Minor-unit
currencystring✅
paymentPurposestring✅Purpose code
orderRefstring—Your order number
cashierRefstring—Cashier/branch reference
otpDestination, otpChannel, localestring—For buyer confirmation

⚠️ One of buyerWalletNo or buyerPhone is required.

curl -X POST "https://api-gateway.lunixi.com/api/v1/wallet/payments/buy" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-ORD-4471" \
  -H "X-Key-Id: …" -H "X-Signature: …" \
  -d '{
    "buyerPhone": "+905321234567",
    "amount": "18900",
    "currency": "TRY",
    "paymentPurpose": "PURCHASE",
    "orderRef": "ORD-4471"
  }'

The customer approves

The payment does not complete until the customer approves. Approval arrives two ways:

  • From the portal/app — the customer calls POST /api/portal/v1/wallet/me/payments/{requestId}/approve in your app.
  • From your server — POST /api/v1/wallet/payments/{id}/approve carrying buyerEndUserId, integrityHash, nonce and otpCode. buyerEndUserId is the paying end user and is required; if it does not match the payer of the payment request, the request is rejected with 404.
{
  "status": "success",
  "code": "SUCCESS",
  "data": {
    "operationId": "e9c4a7b2-1f3d-4a6e-b8c5-7d2f0e9a3b16",
    "status": "COMPLETED",
    "ledgerTxnRef": "ltx_01J8ZW…"
  }
}

Confirm the result

GET /api/v1/wallet/payments/{id}
⚠️

Gate "payment succeeded, release the goods" on COMPLETED only. INITIALIZED and CHALLENGED merely mean a request was opened; the customer may never approve it.

When the outcome is not final yet: INDETERMINATE

The approval response may carry status: "INDETERMINATE". This is not a rejection: the amount may already have been debited from the customer's wallet, but the final state of the payment is not settled yet. The wallet settles it on its own shortly, as COMPLETED or FAILED/REVERSED.

⚠️

When you see INDETERMINATE, do not treat the payment as failed, do not send the customer to another payment method and do not open a new payment request — if the first payment completes, the customer is charged twice. Query the result with GET /api/v1/wallet/payments/{id} or resend the approval with the same Idempotency-Key; once the payment settles, the final state is returned.

Code collection (CPM)

At a physical checkout the customer generates a single-use code in the app; the cashier scans it and collects.

The customer generates a code

POST   /api/portal/v1/wallet/me/payment-code    # generate
DELETE /api/portal/v1/wallet/me/payment-code    # revoke

The code is short-lived and single-use — it is consumed the moment it is scanned.

The business collects

POST /api/v1/wallet/collect
FieldTypeRequiredDescription
codestring✅The code the customer showed
amountstring✅Minor-unit
currencystring✅
orderRefstring—Receipt/order reference
paymentPurposestring—
localestring—
⚠️

The code is single-use: on a network error at the till, do not rescan the code — repeat the same request with the same Idempotency-Key. If the code is already consumed, ask the customer for a new one.

When you display the code, show the remaining validity too. An expired code fails at the till as "invalid", which cashiers usually misread as a balance problem.

Commission

Payments into a business account are subject to the program's commission schedule. Commission is computed at payment time and deducted from the business's receivable.

GET /api/v1/wallet/business-accounts/{id}/commission

Commission schedules are versioned and approved from the merchant panel: Panel — Limit & Fee Schemes.

Fees and commission are different: a fee is charged to whoever performs the operation, commission is charged to the collecting business. They appear in separate fields on the operation (feeTotal, commissionTotal).

Refunds

Refunding a wallet payment credits the wallet. Refunding a card payment into a wallet is a separate product: Refund to Wallet.

Webhooks

EventWhen
wallet.payment.completedPayment completed and credited to the business
wallet.payment.failedPayment failed
wallet.payment.reversedA completed payment was reversed
wallet.refund.creditedA refund was credited to the wallet
wallet.refund.reversedA refund was reversed

Common mistakes

SymptomCauseFix
Payment stuck at CHALLENGEDThe customer never approvedIt becomes EXPIRED; open a new request
Approval returned INDETERMINATEThe outcome is not final yet; the amount may have been debitedDo not treat it as failed; query it or re-approve with the same key (details)
Code reported "invalid"Expired or already consumedAsk the customer for a new code
Commission differs from expectationThe active commission schedule version changedCheck commissionScheduleVersionId on the operation
Buyer not foundThe phone is not registered in the programCheck first with POST /wallet/wallets/lookup