Skip to main content

Migration Overview

The migration endpoints import an existing user base into the platform in bulk, instead of creating users one by one through Create User. All of them require the user:create permission.

Two wallet flows​

Every migrated user gets one wallet, on the chain the row names in chain_id (a chains id, listed by GET /v2/chains). The flows differ in where that wallet comes from:

FlowWhen to useWhat the platform recordsEndpoints
With walletThe user already has an address (self-custody, or brought from another system).A self-custody wallet (custody: false) holding the given address on the given chain, labelled with the user's email (or external_id)./v2/users/migrate/with-wallet, /with-wallet/bulk, /with-wallet/csv
Without walletThe platform should create a custody wallet for the user.A custody wallet (custody: true) created asynchronously on the chain's default network./v2/users/migrate/without-wallet, /without-wallet/bulk, /without-wallet/csv

The address is stored on the wallet, not on the user: the user record has no address field. Read the wallet with List User Wallets.

Each flow has three transports:

  • Single: JSON body with one user; answers the created user.
  • Bulk: JSON body with up to 500 users; answers per-row results.
  • CSV: multipart upload (form field file, at most 5 MB and 500 data rows); answers per-row results.

Identity rules (KVKK-aware)​

Personal information (email, name, surname) is optional. Each row needs at least one of:

  • email: the standard identifier;
  • external_id: an opaque identifier from your system, for when personal data must not be stored (KVKK / GDPR).
emailexternal_idOutcome
✓—Classic: email, name and surname stored
—✓KVKK: only external_id (and any name/surname you send) stored
✓✓Both stored; duplicate checks run on both
——Row rejected with missing_email_and_external_id

external_id is trimmed; an empty value counts as absent.

KYC behaviour (kyc_sent)​

kyc_sentWhat the platform does
false (default)Sends its KYC token(s) to the wallet on every network of the chain, asynchronously. For the without-wallet flow this happens once the wallet has been created.
trueSends nothing and records the wallet as already KYC-verified: on every network of the chain (with wallet) or on the chain's default network (without wallet).

KYC is recorded on the wallet, never on the user: the migrated user's kyc field is always null. Read it with Get User Wallet.

Row checks and reasons​

Checks run per row, in this order. A row that fails one is skipped, not failed, so a bad row does not abort a bulk import.

ReasonCondition
missing_email_and_external_idNeither identifier supplied
duplicate_emailA user with this email already exists
duplicate_external_idA user with this external_id already exists
duplicate_wallet(with wallet) A user already holds a wallet with this address, compared case-insensitively
invalid_chain(with wallet) chain_id names no chain

A row is failed when something unexpected happens (a store or queue failure, a chain_id that is not a valid id); its reason is then free text.

In the without-wallet flow the chain is resolved only after the user has been stored. A row whose chain_id does not resolve therefore fails with the user already created and no wallet. Do not re-run such a row (it would now be skipped as a duplicate); add the wallet with Create User Wallet.

Bulk response shape​

All bulk and CSV endpoints answer 200 with counts and one result per row, in request order:

{
"total": 3,
"success": 2,
"skipped": 1,
"failed": 0,
"results": [
{ "email": "a@example.com", "user_id": "6913872755654191dede37da", "status": "created" },
{ "external_id": "EXT-1", "user_id": "6913872755654191dede37db", "status": "created" },
{ "email": "dup@example.com", "status": "skipped", "reason": "duplicate_email" }
]
}
  • status is created, skipped or failed.
  • reason is present on skipped and failed rows.
  • user_id is present only on created rows.
  • email and external_id echo the row's identifiers; each is omitted when the row did not send it.

Single mode failures​

The single endpoints answer a skipped row with HTTP 400, code 10015, and the reason as the message (for example "message": "duplicate_email"), instead of a per-row report.

See Errors for the error format.