Create an End User
- 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:manageorwallet:program:manageon your key.
POST /api/v1/wallet/end-usersCreates 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
| Header | Required | Rule |
|---|---|---|
| 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.
| Field | Type | Required | Rule and meaning |
|---|---|---|---|
programId | string (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. |
accountType | string | ✅ | PERSONAL (person) or BUSINESS (business). |
phone | string | ✅ | 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). |
externalCustomerId | string | — | The customer id in your system, up to 128 characters. Unique within the program; case-sensitive, leading and trailing whitespace is trimmed. |
displayName | string | — | Display name, up to 120 characters. Only the masked form is stored (displayNameMasked). |
kycRef | string | — | Your reference to the user's KYC record, up to 128 characters. Stored and returned as kycRef; it does not set the KYC level. |
kycLevelCache | string | — | 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. |
corporateAccountId | string (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. |
defaultCurrency | string | — | 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:
- If
corporateAccountIdis given, the corporate account's funding currency. If you also senddefaultCurrency, it must equal the funding currency; otherwise the request is rejected with400 WALLET_VALIDATION. - If
defaultCurrencyis given, that currency. - 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 coversprogramId,accountType,phone,externalCustomerId,corporateAccountIdanddefaultCurrency. - 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
| HTTP | Code | When |
|---|---|---|
| 400 | VALIDATION_ERROR | The 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 |
| 400 | WALLET_VALIDATION | The 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 |
| 403 | WALLET_KYC_LEVEL_UNVERIFIED | A level other than NONE was sent as kycLevelCache |
| 403 | WALLET_INSUFFICIENT_SCOPE | The key lacks wallet:enduser:manage or wallet:program:manage |
| 404 | WALLET_NOT_FOUND | programId or corporateAccountId is not one of your records |
| 409 | WALLET_ENDUSER_EXISTS | An end user with this phone or this externalCustomerId already exists in the program |
| 409 | WALLET_STATE_INVALID | The program is not ACTIVE |
| 409 | WALLET_CURRENCY_NOT_ALLOWED | defaultCurrency is not enabled in your program |
| 409 | WALLET_PROGRAM_CURRENCY_UNDESIGNATED | defaultCurrency was not sent and the program has no main currency set |
| 409 | WALLET_IDEMPOTENCY_CONFLICT | The same Idempotency-Key was used with a different request |
| 409 | WALLET_IDEMPOTENCY_IN_PROGRESS | The first request sent with the same key is still being processed; retry shortly with the same key |