Wallet
End users
Create

Create an End User

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
The same action exists in the merchant panel. This endpoint requires wallet:enduser:manage or wallet:program:manage on your key.
POST /api/v1/wallet/end-users

Creates the end user and, in the same operation, opens their default wallet with a single account. The response returns the user together with this wallet and its account.

Headers

HeaderRequiredRule
Signature headers✅Authentication
Idempotency-Key✅1–128 characters after leading and trailing whitespace is trimmed. Missing, empty or longer values are rejected with 400.

Body fields

The body may contain only the fields below; any other field is rejected with 400 VALIDATION_ERROR.

FieldTypeRequiredRule and meaning
programIdstring (UUID)✅Your program id (GET /api/v1/wallet/programs/me). A program that is not yours returns 404 WALLET_NOT_FOUND; a program that is not ACTIVE returns 409 WALLET_STATE_INVALID.
accountTypestring✅PERSONAL (person) or BUSINESS (business).
phonestring✅8–20 characters; only digits, +, space, (, ) and -. Non-digits are removed and a leading 00 is dropped; the remaining digits must number 8–15, otherwise 400 WALLET_VALIDATION. Send it in international format with the country code (+905321234567).
externalCustomerIdstring—The customer id in your system, up to 128 characters. Unique within the program; case-sensitive, leading and trailing whitespace is trimmed.
displayNamestring—Display name, up to 120 characters. Only the masked form is stored (displayNameMasked).
kycRefstring—Your reference to the user's KYC record, up to 128 characters. Stored and returned as kycRef; it does not set the KYC level.
kycLevelCachestring—Up to 32 characters. Omit it or send NONE: an end user is always opened at level NONE. Sending LOW, SUBSTANTIAL or HIGH rejects the request with 403 WALLET_KYC_LEVEL_UNVERIFIED. Raise the level after creation: KYC Levels.
corporateAccountIdstring (UUID)—Links the user to an employer (corporate account) as an employee. The corporate account must be yours, ACTIVE and in the same program. See Corporate Accounts.
defaultCurrencystring—Currency of the account in the default wallet; 3–8 letters, upper-cased. Must be enabled in your program, otherwise 409 WALLET_CURRENCY_NOT_ALLOWED.

How the phone is stored

The phone number is not stored in plain text. A one-way digest (for lookups and uniqueness) and a masked form are derived from the normalized digits. The masked form keeps the first 4 and last 2 digits visible: +905321234567 → 9053******67. Responses return the phone only as phoneMasked.

⚠️

Uniqueness is based on the normalized digits. +90 532 123 45 67, 905321234567 and 00905321234567 are the same number; 05321234567 is treated as a different number. Always send the number with its country code.

How the display name is stored

displayName is masked so that the first letter of each word stays visible and the rest becomes * (at most 8 stars per word, at most 64 characters in total): Ayşe Yılmaz → A*** Y*****. The clear form is not stored and cannot be read back. The masked name is shown in wallet lookup to confirm the recipient; without it, the masked phone is shown there.

Opening currency

The default wallet opens with a single account. Its currency is determined in this order:

  1. If corporateAccountId is given, the corporate account's funding currency. If you also send defaultCurrency, it must equal the funding currency; otherwise the request is rejected with 400 WALLET_VALIDATION.
  2. If defaultCurrency is given, that currency.
  3. Otherwise, your program's main currency.

If your program has no main currency set and you did not send defaultCurrency, the request is rejected with 409 WALLET_PROGRAM_CURRENCY_UNDESIGNATED and nothing is created. No guess is made from the program's currency list. Open accounts in other currencies afterwards: Wallets & Accounts.

Idempotency

Idempotency-Key is required and binds the request so it is processed once:

  • Repeating the same request with the same key returns the first successful response as is; no second user is created.
  • Using the same key with a different request returns 409 WALLET_IDEMPOTENCY_CONFLICT. The comparison covers programId, accountType, phone, externalCustomerId, corporateAccountId and defaultCurrency.
  • If the first request failed, the key is released; you can retry with the same key.

All scenarios and the recommended integration pattern: Creating Twice.

Example

curl -X POST "https://api-gateway.lunixi.com/api/v1/wallet/end-users" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: enduser-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":"Ayşe Yılmaz","defaultCurrency":"TRY"}'
{
  "status": "success",
  "code": "SUCCESS",
  "message": "…",
  "data": {
    "endUser": {
      "id": "8d4c2a1f-3b6e-4f7a-9c5d-2e8b1a0f6c34",
      "programId": "5b9e2f3a-7c41-4d8e-a6f2-9e1b3c4d5f60",
      "accountType": "PERSONAL",
      "externalCustomerId": "crm-88213",
      "corporateAccountId": null,
      "corporateOffboardedAt": null,
      "kycRef": null,
      "kycLevelCache": null,
      "kycLevelRefreshedAt": null,
      "phoneMasked": "9053******67",
      "displayNameMasked": "A*** Y*****",
      "merchantCategory": null,
      "status": "ACTIVE",
      "createdAt": "2026-10-11T09:15:02.418Z",
      "updatedAt": "2026-10-11T09:15:02.418Z"
    },
    "defaultWallet": {
      "id": "1e7f9a3b-5c2d-4a8e-b6f1-3d9c0e2a7b58",
      "endUserId": "8d4c2a1f-3b6e-4f7a-9c5d-2e8b1a0f6c34",
      "walletNo": "5810763412",
      "name": "Cüzdanım",
      "isDefault": true,
      "status": "ACTIVE",
      "accounts": [
        {
          "id": "c2a5e8f1-9d3b-4c7a-8e6f-5b1d2a9c0e47",
          "walletId": "1e7f9a3b-5c2d-4a8e-b6f1-3d9c0e2a7b58",
          "currency": "TRY",
          "accountCode": 5000,
          "fullWalletNo": "58107634125000",
          "status": "ACTIVE"
        }
      ],
      "createdAt": "2026-10-11T09:15:02.418Z"
    }
  },
  "requestId": "…"
}

The meaning of the endUser fields is on Detail & Lookup. In money movements you use defaultWallet.accounts[].id (the walletAccountId) as the source account. The default wallet is always named Cüzdanım.

A new user's kycLevelCache is null: the user is at level NONE. Which operations are available at this level is described in KYC Levels.

Errors

HTTPCodeWhen
400VALIDATION_ERRORThe body breaks a rule or contains an undefined field (details.errors lists which field was rejected and why)
400—Idempotency-Key missing, empty or longer than 128 characters
400WALLET_VALIDATIONThe phone does not have 8–15 digits after normalization; the corporate account is not ACTIVE or belongs to another program; defaultCurrency conflicts with the corporate account's funding currency
403WALLET_KYC_LEVEL_UNVERIFIEDA level other than NONE was sent as kycLevelCache
403WALLET_INSUFFICIENT_SCOPEThe key lacks wallet:enduser:manage or wallet:program:manage
404WALLET_NOT_FOUNDprogramId or corporateAccountId is not one of your records
409WALLET_ENDUSER_EXISTSAn end user with this phone or this externalCustomerId already exists in the program
409WALLET_STATE_INVALIDThe program is not ACTIVE
409WALLET_CURRENCY_NOT_ALLOWEDdefaultCurrency is not enabled in your program
409WALLET_PROGRAM_CURRENCY_UNDESIGNATEDdefaultCurrency was not sent and the program has no main currency set
409WALLET_IDEMPOTENCY_CONFLICTThe same Idempotency-Key was used with a different request
409WALLET_IDEMPOTENCY_IN_PROGRESSThe first request sent with the same key is still being processed; retry shortly with the same key