Migration Overview
User Migration endpoints let an integration import an existing user base into the tokenization platform via API. They cover the cases where users are created in bulk during an onboarding cut-over instead of one-by-one through the standard Create User flow.
Two Wallet Flows
The platform can either use a wallet address you already have for the user, or generate a custodial wallet on your behalf. The migration endpoints split along this axis:
| Flow | When to use | Endpoints |
|---|---|---|
| With Wallet | The user already has a wallet address (self-custody or migrated from elsewhere). | /migrate/with-wallet, /migrate/with-wallet/bulk, /migrate/with-wallet/csv |
| Without Wallet | The platform should generate a wallet for the user (asynchronous). | /migrate/without-wallet, /migrate/without-wallet/bulk, /migrate/without-wallet/csv |
Each flow has three transports:
- Single — JSON body with a single user object.
- Bulk — JSON body with an array of up to 500 users; per-row results returned.
- CSV — Multipart upload (form field
file); per-row results returned. Maximum file size: 5 MB, maximum rows: 500.
Identity Rules (KVKK-Aware)
Personal information (email, name, surname) is optional in migration. At least one of the following two identifiers must be present per row:
email— standard identifier.external_id— opaque identifier from your system; used when you must not store personal data under KVKK / GDPR.
Both can be supplied together. Behaviour by identifier combination:
email | external_id | Outcome |
|---|---|---|
| ✓ | — | Classic mode — email/name/surname stored. |
| — | ✓ | KVKK mode — only external_id stored. email, name, surname are dropped. |
| ✓ | ✓ | Hybrid mode — both stored. Duplicate checks run on both. |
| — | — | Row rejected with reason missing_email_and_external_id. |
For with-wallet endpoints, wallet_address is always required regardless of identity mode.
KYC Behaviour (kyc_sent)
Each migration row carries a kyc_sent flag indicating whether KYC has already been delivered for this user in your previous system.
kyc_sent | What the platform does |
|---|---|
false (default) | Standard flow — the platform publishes a KYC token event for this user. |
true | The user is marked as already KYC-verified (kyc.is_verified=true, kyc.token_sent=true) and no KYC event is published. The wallet-generation event for the without-wallet flow propagates skip_kyc=true to the downstream consumer. |
Duplicate Handling
Per-row duplicate checks run before user creation. A duplicate is skipped, not failed, so a single bad row does not abort a bulk import.
| Reason | Condition |
|---|---|
duplicate_email | An existing user has the same email. |
duplicate_external_id | An existing user has the same external_id (trim-normalized). |
duplicate_wallet | (with-wallet only) An existing user has the same wallet address (case-insensitive). |
missing_email_and_external_id | Neither identifier supplied. |
Bulk Response Shape
All bulk and CSV endpoints return the same response shape — an aggregate header and per-row results:
{
"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∈created|skipped|failed.reasonis present onskippedandfailedrows; absent oncreated.user_idis present only forcreatedrows.- Either
emailorexternal_id(or both) is always present, mirroring the row's identity.
When Single Mode Fails
For the single (non-bulk) endpoints, duplicates and missing-identity errors return HTTP 400 with a message containing the same reason string, instead of a per-row report.
Authorization
All migration endpoints require a valid integration JWT (Authorization: Bearer …) and respect the same client/mTLS/IP/rate/usage middleware as the rest of the integration API. See the Integration Overview for details.