Wallet
Quickstart

Quickstart

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 the s2s surface — call them from your own server with a signed request.

This guide takes you from zero to a working wallet flow: create a user → top up → transfer → confirm. Every step uses a path that runs in production today; nothing here depends on a provider that is not yet connected.

All amounts are minor-unit strings: "amount": "10050" means ₺100.50. Every request is Ed25519-signed — signature headers are abbreviated as … below. Send request bodies as compact JSON (no line breaks or extra spaces) and compute Digest over exactly that string; the bodies below are spread over several lines only for readability.

Verify signing

Before moving money, prove your signing works with the simplest endpoint:

curl "https://api-gateway.lunixi.com/api/v1/wallet/ping" \
  -H "X-Key-Id: $LUNIXI_KEY_ID" \
  -H "X-Date: $DATE" -H "X-Nonce: $NONCE" -H "X-Signature: $SIG"

The key needs a set-up permission (wallet:program:manage or wallet:enduser:manage) for this call. Do not continue until you get a 200. If not, see Authentication → Error codes.

Find your program

curl "https://api-gateway.lunixi.com/api/v1/wallet/programs/me" \
  -H "X-Key-Id: …" -H "X-Date: …" -H "X-Nonce: …" -H "X-Signature: …"

Note your program's id: you send it as programId when creating end users, and in other calls that take a program.

Create an end user

Users are unique by phone within a program — a second call with the same phone and a new key does not create another user; it returns 409 WALLET_ENDUSER_EXISTS, while a repeat with the same Idempotency-Key returns the first response. All fields and rules: Create an End User.

curl -X POST "https://api-gateway.lunixi.com/api/v1/wallet/end-users" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: signup-crm-88213" \
  -H "X-Key-Id: …" -H "X-Date: …" -H "X-Nonce: …" -H "Digest: …" -H "X-Signature: …" \
  -d '{
    "programId": "5b9e2f3a-7c41-4d8e-a6f2-9e1b3c4d5f60",
    "accountType": "PERSONAL",
    "phone": "+905321234567",
    "externalCustomerId": "crm-88213",
    "displayName": "A. Yilmaz"
  }'

The response returns the user together with the default wallet and its accounts:

{
  "status": "success",
  "code": "SUCCESS",
  "data": {
    "endUser": { "id": "8d4c2a1f-3b6e-4f7a-9c5d-2e8b1a0f6c34", "accountType": "PERSONAL", "status": "ACTIVE" },
    "defaultWallet": {
      "id": "1e7f9a3b-5c2d-4a8e-b6f1-3d9c0e2a7b58",
      "walletNo": "5810763412",
      "name": "Cüzdanım",
      "isDefault": true,
      "accounts": [
        { "id": "c2a5e8f1-9d3b-4c7a-8e6f-5b1d2a9c0e47", "currency": "TRY", "fullWalletNo": "58107634125000", "status": "ACTIVE" }
      ]
    }
  }
}

externalCustomerId is your identifier. If you would rather not store ours, reach the user with GET /wallet/end-users/by-external/crm-88213?programId=….

Open an extra account if needed

The default wallet opens with a single account, in your program's main currency (or the currency you sent as defaultCurrency). For another currency:

curl -X POST "https://api-gateway.lunixi.com/api/v1/wallet/wallets/1e7f9a3b-5c2d-4a8e-b6f1-3d9c0e2a7b58/accounts" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: acct-crm-88213-usd" \
  -H "X-Key-Id: …" -H "X-Signature: …" \
  -d '{ "currency": "USD" }'

Top up by card

Card top-ups are delegated to the payment stack: the response gives you either hosted checkout content or a 3D Secure redirect.

curl -X POST "https://api-gateway.lunixi.com/api/v1/wallet/topups/card" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: topup-crm-88213-1" \
  -H "X-Key-Id: …" -H "X-Signature: …" \
  -d '{
    "creditWalletAccountId": "c2a5e8f1-9d3b-4c7a-8e6f-5b1d2a9c0e47",
    "amount": "25000",
    "currency": "TRY",
    "returnUrl": "https://app.acme.com/wallet/topup/result"
  }'
{
  "status": "success",
  "code": "SUCCESS",
  "data": {
    "operationId": "4f1b8d2e-6a9c-4b3e-8f7d-1c5e9a2b6d08",
    "paymentId": "9c6f2b1a-4e8d-4a3f-b5e6-2d7c1a9b8e30",
    "status": "INITIALIZED",
    "checkoutFormContent": "<script …>",
    "redirectUrl": ""
  }
}

Mount checkoutFormContent in the user's browser; card data never touches your server. When the user completes payment the operation becomes COMPLETED and wallet.topup.completed fires.

⚠️

Do not learn the top-up outcome from the user returning to returnUrl — the browser may never come back. The webhook is the source of truth; confirm with GET /wallet/topups/{operationId}.

Check the balance

curl "https://api-gateway.lunixi.com/api/v1/wallet/end-users/8d4c2a1f-3b6e-4f7a-9c5d-2e8b1a0f6c34/balances" \
  -H "X-Key-Id: …" -H "X-Signature: …"
{
  "status": "success",
  "code": "SUCCESS",
  "data": {
    "balances": [
      {
        "walletAccountId": "c2a5e8f1-9d3b-4c7a-8e6f-5b1d2a9c0e47",
        "currency": "TRY",
        "balance": "25000",
        "fullWalletNo": "58107634125000"
      }
    ]
  }
}

Quote the transfer first

For outbound money you must call quote first — the user has to see the fee before the debit.

curl -X POST "https://api-gateway.lunixi.com/api/v1/wallet/transfers/w2w/quote" \
  -H "Content-Type: application/json" \
  -H "X-Key-Id: …" -H "X-Signature: …" \
  -d '{
    "endUserId": "8d4c2a1f-3b6e-4f7a-9c5d-2e8b1a0f6c34",
    "sourceWalletAccountId": "c2a5e8f1-9d3b-4c7a-8e6f-5b1d2a9c0e47",
    "destinationPhone": "+905339876543",
    "amount": "10050",
    "currency": "TRY"
  }'
{
  "status": "success",
  "code": "SUCCESS",
  "data": {
    "feeTotal": "250",
    "taxTotal": "125",
    "totalCharge": "10425",
    "requiredAmount": "10425",
    "maskedDestinationName": "Meh*** O***",
    "sourceBalance": "25000",
    "sufficientBalance": true,
    "limitBlocked": false,
    "scheduleVersionId": "2d8b5e1c-7f4a-4c9e-a3b6-8e1f5c2d9a74"
  }
}

If sufficientBalance is false or limitBlocked is true, do not start the transfer — show the user why. maskedDestinationName exists so the sender can confirm the recipient is the right person.

Initialize the transfer

curl -X POST "https://api-gateway.lunixi.com/api/v1/wallet/transfers/w2w" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: p2p-crm-88213-1" \
  -H "X-Key-Id: …" -H "X-Signature: …" \
  -d '{
    "endUserId": "8d4c2a1f-3b6e-4f7a-9c5d-2e8b1a0f6c34",
    "sourceWalletAccountId": "c2a5e8f1-9d3b-4c7a-8e6f-5b1d2a9c0e47",
    "destinationPhone": "+905339876543",
    "amount": "10050",
    "currency": "TRY",
    "scheduleVersionId": "2d8b5e1c-7f4a-4c9e-a3b6-8e1f5c2d9a74",
    "otpDestination": "+905321234567",
    "otpChannel": "SMS"
  }'
{
  "status": "success",
  "code": "SUCCESS",
  "data": {
    "operationId": "7a3e9c1d-2b4f-4e8a-9d6c-0f5b8e2a1c93",
    "status": "CHALLENGED",
    "otpRequired": true,
    "otpChannel": "SMS",
    "integrityHash": "9c1f…",
    "nonce": "4b82…"
  }
}

Funds are reserved; nothing has moved yet. When otpRequired is true, collect the code from the user.

Complete the transfer

curl -X POST "https://api-gateway.lunixi.com/api/v1/wallet/transfers/w2w/7a3e9c1d-2b4f-4e8a-9d6c-0f5b8e2a1c93/complete" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: p2p-crm-88213-1-complete" \
  -H "X-Key-Id: …" -H "X-Signature: …" \
  -d '{
    "endUserId": "8d4c2a1f-3b6e-4f7a-9c5d-2e8b1a0f6c34",
    "operationId": "7a3e9c1d-2b4f-4e8a-9d6c-0f5b8e2a1c93",
    "integrityHash": "9c1f…",
    "nonce": "4b82…",
    "otpCode": "482913"
  }'
{
  "status": "success",
  "code": "SUCCESS",
  "data": {
    "operationId": "7a3e9c1d-2b4f-4e8a-9d6c-0f5b8e2a1c93",
    "status": "COMPLETED",
    "ledgerTxnRef": "ltx_01J8ZU…",
    "receiptUrl": "https://api-gateway.lunixi.com/api/v1/wallet/operations/7a3e9c1d-2b4f-4e8a-9d6c-0f5b8e2a1c93/receipt"
  }
}
⚠️

integrityHash and nonce must be carried over verbatim. The nonce is single-use; if complete errors, query the operation instead of retrying with the same nonce.

Wire up webhooks

Learn async outcomes from webhooks, not polling. Register your endpoint in the panel and subscribe to:

EventWhen
wallet.topup.completedA card/bank top-up landed in the balance
wallet.topup.failedThe top-up failed
wallet.transfer.completedThe transfer completed
wallet.transfer.failedThe transfer failed and the reservation was released

Envelope and signature verification: Webhooks.

Next steps

  • Core Concepts — program, account, operation vocabulary
  • Limits and Fees — to understand rejections
  • Portal & SDK — if you are building the end-user app
  • Roadmap — status of bank integrations, bill pay, ATM and more