End User Detail & Lookup
- 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
- Reading by id also exists in the merchant panel. These endpoints require a wallet read permission (
wallet:enduser:read) on your key.
You can read an end user in two ways: by the id we issued, or by the externalCustomerId you sent at creation. Both endpoints return the same response.
Read by id
GET /api/v1/wallet/end-users/{id}id must be a UUID; otherwise the request is rejected with 400.
curl "https://api-gateway.lunixi.com/api/v1/wallet/end-users/8d4c2a1f-3b6e-4f7a-9c5d-2e8b1a0f6c34" \
-H "X-Key-Id: …" -H "X-Date: …" -H "X-Nonce: …" -H "X-Signature: …"Lookup by externalCustomerId
GET /api/v1/wallet/end-users/by-external/{externalCustomerId}?programId={programId}| Parameter | In | Required | Description |
|---|---|---|---|
externalCustomerId | path | ✅ | The value you sent at creation. URL-encode it. |
programId | query | ✅ | Always required, because externalCustomerId is unique within a program. Without it the request is rejected with 400 WALLET_VALIDATION. |
Matching is exact: a difference in letter case counts as a different customer; only leading and trailing whitespace is trimmed. If there is no match, the phone or any other field is not tried; the response is 404 WALLET_NOT_FOUND.
curl "https://api-gateway.lunixi.com/api/v1/wallet/end-users/by-external/crm-88213?programId=5b9e2f3a-7c41-4d8e-a6f2-9e1b3c4d5f60" \
-H "X-Key-Id: …" -H "X-Date: …" -H "X-Nonce: …" -H "X-Signature: …"Response
{
"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": "LOW",
"kycLevelRefreshedAt": "2026-10-11T10:02:44.105Z",
"phoneMasked": "9053******67",
"displayNameMasked": "A*** Y*****",
"merchantCategory": null,
"status": "ACTIVE",
"createdAt": "2026-10-11T09:15:02.418Z",
"updatedAt": "2026-10-11T10:02:44.105Z"
}
},
"requestId": "…"
}Response fields
| Field | Type | Meaning |
|---|---|---|
id | UUID | The end user id. Used as {id} / endUserId in every other endpoint. |
programId | UUID | The program the user belongs to. |
accountType | string | PERSONAL or BUSINESS. |
externalCustomerId | string | null | Your customer id; null if not sent. |
corporateAccountId | UUID | null | The employer (corporate account) the user is linked to as an employee. null = not linked to an employer. |
corporateOffboardedAt | date | null | When the user last left an employer. If corporateAccountId is also set, the user was linked again after leaving. See Corporate Accounts. |
kycRef | string | null | The KYC reference you sent at creation. |
kycLevelCache | string | null | The recorded KYC level: LOW, SUBSTANTIAL or HIGH. null = no level recorded yet; the user is at level NONE. |
kycLevelRefreshedAt | date | null | When the level was last recorded. |
phoneMasked | string | Masked phone: first 4 and last 2 digits visible (9053******67). The clear phone is never returned. |
displayNameMasked | string | null | Masked display name (A*** Y*****); null if no name was sent. |
merchantCategory | string | null | The category of a business (BUSINESS) record; assigned in the merchant panel. null = no category assigned. Always null for personal records. In a program with a category rule, spending at an uncategorized business is rejected. |
status | string | ACTIVE, FROZEN or CLOSED — see Statuses. |
createdAt, updatedAt | date | ISO 8601, UTC. |
kycLevelCache is the recorded level. The effective level used in limit and permission decisions can be lower if your program defines a level validity period. Read the effective level with GET /api/v1/wallet/end-users/{id}/kyc-level: KYC Levels.
Both endpoints return the user regardless of status; FROZEN and CLOSED users can be read too. The balances endpoint lists the user's accounts and their account numbers (fullWalletNo).
404 behaviour
A user that belongs to another merchant and a user that does not exist at all get the same response:
{
"status": "failure",
"code": "WALLET_NOT_FOUND",
"message": "…",
"statusCode": 404,
"requestId": "…"
}The response does not reveal whether a record exists in another account. On by-external, sending a programId other than the one the user belongs to also returns 404.
Errors
| HTTP | Code | When |
|---|---|---|
| 400 | — | {id} is not a UUID |
| 400 | WALLET_VALIDATION | programId missing on a by-external call |
| 403 | WALLET_INSUFFICIENT_SCOPE | The key has no wallet read permission |
| 404 | WALLET_NOT_FOUND | The user does not exist or is not yours |