Wallet
End users
Creating Twice

Creating the Same User Twice

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.

POST /api/v1/wallet/end-users has two separate safeguards:

  1. Idempotency-Key — prevents the same request from being processed twice.
  2. 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

#SituationResult
aSame Idempotency-Key, same body200 — the first successful response is returned as is (same endUser.id, same defaultWallet). Nothing new is created.
bSame Idempotency-Key, different body409 WALLET_IDEMPOTENCY_CONFLICT — nothing changes.
cNew Idempotency-Key, same phone in the same program409 WALLET_ENDUSER_EXISTS — the existing user is not returned.
dNew Idempotency-Key, same externalCustomerId with a different phone in the same program409 WALLET_ENDUSER_EXISTS
eSame phone in another of your programsSuccess — 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 FROZEN or CLOSED user either.
  • Phones are compared by their normalized digits: +90 532 123 45 67 and 905321234567 are the same, 05321234567 is 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

  1. Derive the key from your customer. Build the Idempotency-Key deterministically from your own customer id (for example enduser-crm-88213) and send the same id as externalCustomerId. Every repeat after a timeout or network error then receives the first response.

  2. 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.

  3. Read the user on conflict. When you get 409 WALLET_ENDUSER_EXISTS, read the user by externalCustomerId:

    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_FOUND is returned, the phone belongs to a user in the program registered under another externalCustomerId (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.
⚠️

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.