Creating the Same User Twice
- 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.
POST /api/v1/wallet/end-users has two separate safeguards:
- Idempotency-Key — prevents the same request from being processed twice.
- Uniqueness within the program — a program never gets a second end user with the same phone or the same
externalCustomerId.
An existing user is not returned to a new request; a uniqueness violation is always reported as an error. The only ways to get the existing record are to repeat with the same Idempotency-Key or to read the user separately.
Scenarios
| # | Situation | Result |
|---|---|---|
| a | Same Idempotency-Key, same body | 200 — the first successful response is returned as is (same endUser.id, same defaultWallet). Nothing new is created. |
| b | Same Idempotency-Key, different body | 409 WALLET_IDEMPOTENCY_CONFLICT — nothing changes. |
| c | New Idempotency-Key, same phone in the same program | 409 WALLET_ENDUSER_EXISTS — the existing user is not returned. |
| d | New Idempotency-Key, same externalCustomerId with a different phone in the same program | 409 WALLET_ENDUSER_EXISTS |
| e | Same phone in another of your programs | Success — a separate end user is created in that program. Uniqueness is per program. |
a — Same key, same body
If the first request completed successfully, every repeat with the same key returns the first response. That response reflects the state at creation time; if the user has been frozen since, the repeated response does not show it. Read the current state with the detail endpoint.
- If the first request is still being processed, the repeat gets
409 WALLET_IDEMPOTENCY_IN_PROGRESS. Retry shortly with the same key. - If the first request failed (for example with
WALLET_ENDUSER_EXISTS), the key is released; a repeat with the same key is evaluated again.
"Same body" is compared on these fields: programId, accountType, phone, externalCustomerId, corporateAccountId, defaultCurrency. The phone is compared after normalization. displayName, kycRef and kycLevelCache are not compared: if you change them and repeat with the same key, the first response is returned and the new values are not applied.
b — Same key, different body
If any of the fields above differs, the request is rejected with 409 WALLET_IDEMPOTENCY_CONFLICT. This means you accidentally used one key for two different customers; generate a new key for the new customer.
c — Same phone, same program
The phone is unique within a program. If an end user with this phone already exists in the program, the new request gets 409 WALLET_ENDUSER_EXISTS; the existing user's details are not included in the response.
- The check ignores the user's status: a new user cannot be opened in the same program with the phone of a
FROZENorCLOSEDuser either. - Phones are compared by their normalized digits:
+90 532 123 45 67and905321234567are the same,05321234567is different. Always send the number with its country code.
d — Same externalCustomerId, different phone
externalCustomerId is also unique within a program. The same value gets 409 WALLET_ENDUSER_EXISTS even with another phone. The value is case-sensitive (A1 and a1 differ); only leading and trailing whitespace is trimmed. Users created without an externalCustomerId are not checked against each other.
e — Same phone, another program
Uniqueness is on (program, phone) and (program, externalCustomerId). The same person can exist as separate end users in more than one of your programs; each has their own wallet, balance and limits.
Recommended integration pattern
-
Derive the key from your customer. Build the
Idempotency-Keydeterministically from your own customer id (for exampleenduser-crm-88213) and send the same id asexternalCustomerId. Every repeat after a timeout or network error then receives the first response. -
Do not use random keys. Generating a new key on each attempt turns the repeat into a new request; if the first request succeeded, the second attempt gets
409 WALLET_ENDUSER_EXISTS. -
Read the user on conflict. When you get
409 WALLET_ENDUSER_EXISTS, read the user byexternalCustomerId: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: …"- If a record is returned, it is your customer; store
endUser.id. - If
404 WALLET_NOT_FOUNDis returned, the phone belongs to a user in the program registered under anotherexternalCustomerId(or none). Do not claim that record automatically; verify the phone number with your customer and review the record in the merchant panel if needed.
- If a record is returned, it is your customer; store
Do not read a 409 WALLET_ENDUSER_EXISTS response as "user created". The response does not say which record conflicted; act only after matching the user to your own identifier.