QR
- 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
- QR issue/read is SHARED between s2s and panel (
POST qr,GET qr,GET qr/:id,POST qr/:id/revoke);GET qr/:id/imageandPOST qr/parseare s2s-only. QR producer-code management and terminal provisioning are panel-only.
QR works in both directions: a user generates a code to be paid, or a business shows a code to be paid to. Every code is signed, single-use and time-limited.
QR runs closed loop today: the code you generate is read inside your own ecosystem — your app, your businesses, your terminals. For a common-standard format readable by banks and other payment apps, see Interoperability below.
Generating a code
POST /api/v1/wallet/qr| Field | Type | Required | Description |
|---|---|---|---|
qrType | string | ✅ | P2P_STATIC, P2P_DYNAMIC, MERCHANT_STATIC or MERCHANT_DYNAMIC — person-to-person or business code; a static code carries no amount or expiry, a dynamic code carries both |
currency | string | ✅ | |
amount | string | — | Fixed-amount code. Omit and the payer enters the amount |
targetEndUserId | string | — | The user the code belongs to |
targetExternalCustomerId | string | — | Target by your own customer id |
programId | string | — | Required when you run more than one program |
expiresInSeconds | number | — | Validity window |
metadata | object | — | Your own references (order no, till no, …) |
curl -X POST "https://api-gateway.lunixi.com/api/v1/wallet/qr" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: qr-ORD-4471" \
-H "X-Key-Id: …" -H "X-Signature: …" \
-d '{
"qrType": "MERCHANT_DYNAMIC",
"targetEndUserId": "8d4c2a1f-3b6e-4f7a-9c5d-2e8b1a0f6c34",
"amount": "18900",
"currency": "TRY",
"expiresInSeconds": 300,
"metadata": { "orderRef": "ORD-4471" }
}'Generating an open-amount code (omitting amount) means anyone who sees it can enter any amount. For till use, put the amount inside the code — otherwise what the cashier scanned and what the customer paid can diverge.
Fetching the image
GET /api/v1/wallet/qr/{id}/image?format=svg&scale=6| Parameter | Value | Default |
|---|---|---|
format | svg | png | svg |
scale | Pixels per module | — |
Always fetch the rendered code from this endpoint rather than drawing the payload yourself — if the signature or format version changes, your own rendering silently becomes invalid.
Parsing a code
POST /api/v1/wallet/qr/parse{ "payload": "ver:TRQR1|typ:MERCHANT_DYNAMIC|…" }Parsing tells you whether the code is valid, which program it belongs to, its target and its amount if fixed. Do not write your own parser — the format is versioned and validation depends on the signature.
Paying by QR
In the end-user app the flow is three steps:
Scan and parse
POST /api/portal/v1/wallet/me/qr/parseShow the user who is collecting and how much.
Initialize the payment
POST /api/portal/v1/wallet/me/qr/pay| Field | Required | Description |
|---|---|---|
payload | ✅ | The scanned code |
sourceWalletAccountId | ✅ | Account to pay from |
amount | — | Required when the code is open-amount |
otpDestination, otpChannel, locale | — | For confirmation |
Complete
POST /api/portal/v1/wallet/me/qr/pay/{qrId}/completeWith integrityHash, nonce and otpCode when required.
Revocation and expiry
POST /api/v1/wallet/qr/{id}/revoke
GET /api/v1/wallet/qr/{id}
GET /api/v1/wallet/qrExpired codes become invalid automatically. A revoked code cannot be reused — even if it is still on the user's screen, scanning it is rejected.
The list endpoint (GET /wallet/qr) filters by programId, qrType, status, targetEndUserId and is cursor-paginated. Use it to track active codes instead of keeping your own code ledger.
Interoperability
Today's QR format is closed loop and claims no compliance with an external standard: the code is read only inside your ecosystem, and bank or third-party payment apps cannot decode it.
The reason a common-standard format is not emitted is regulatory rather than technical: publishing a format before the applicable technical guideline and field structure are settled produces a code believed to be compliant that no reader understands. Once that is settled the format will be supported and this box removed.
Today: no standard compliance is claimed. Status: Roadmap.
If an unknown encoding scheme is selected, the platform refuses to generate the code rather than silently falling back to the closed-loop format. That makes "I thought I emitted a standard code" impossible.
Common mistakes
| Symptom | Cause | Fix |
|---|---|---|
| "Invalid" when scanned | Expired or revoked | Generate a new code; show remaining validity |
| Paid amount differs from expected | Open-amount code | Use fixed-amount codes at the till |
| Code not readable in another program | Codes are program-scoped | Generate with the correct programId |
| Self-rendered code not readable | Payload was hand-built | Use GET /wallet/qr/{id}/image |