Self Serve Migration
Self Serve Migration lets you migrate customer identity data into LoginRadius independently using the bulk upsert API. You control the timeline, data transformation, and batch orchestration. The platform handles deduplication, password hash preservation, and audit logging.
Key Capabilities
- Batch upsert: Insert new users or update existing ones in a single API call
- Password hash preservation: Migrate hashed passwords directly, so users keep their existing passwords
- Delta migration: Idempotent re-runs that skip or overwrite duplicates based on your configuration
- B2B support: Migrate users with org-role assignments in one request
- Partial success: Failed records are reported individually; valid records are never blocked by invalid ones
Endpoint
POST /v2/bulk/upsert
Authentication
Required: API Key + API Secret passed via the X-LoginRadius-ApiKey and X-LoginRadius-ApiSecret request headers. The Authorization: Basic header is not accepted for this endpoint. Basic Auth maps to OAuth Client ID/Secret credentials, not the API Key/Secret. OAuth M2M tokens and request signatures are also not accepted for bulk migration.
X-LoginRadius-ApiKey: <apiKey>
X-LoginRadius-ApiSecret: <apiSecret>
The API Key must have the write:migrations scope enabled. Retrieve your API Key and Secret from the LoginRadius Admin Console under Security → API Credentials.
The API Key/Secret can also be passed as query parameters, but this is not recommended, as secrets in query strings are exposed in server access logs, proxies, and browser history. Always prefer the header form.
Required Request Headers
| Header | Value |
|---|---|
X-LoginRadius-ApiKey | <apiKey> |
X-LoginRadius-ApiSecret | <apiSecret> |
Content-Type | application/json |
Request Body
{
"PasswordEncryption": { ... },
"Profiles": [ ... ],
"DeltaMigrationModel": { ... }
}
| Field | Type | Required | Description |
|---|---|---|---|
PasswordEncryption | Object | No | Hash algorithm config applied to all profiles in the batch. If omitted, the app-level password encryption config is used. |
Profiles | Array | Yes | User profiles to insert or upsert. Maximum 500 per request. |
DeltaMigrationModel | Object | No | Controls upsert behavior. Defaults to insert-only if omitted. |
PasswordEncryption Object
Defines how the submitted password hashes were generated. Applied uniformly to all profiles in the batch.
Supported Algorithm Types (Type)
| Value | Algorithm |
|---|---|
pbkdf2 | PBKDF2 |
sha1_pbkdf2 | SHA1 + PBKDF2 |
sha512 | SHA-512 |
hmac_sha1 | HMAC-SHA1 |
hmac_sha256 | HMAC-SHA256 |
argon2i | Argon2i |
argon2id | Argon2id |
Refer to the Hashing Algorithm documentation for details.
For any hash algorithm not in this list (e.g. bcrypt, scrypt), contact support@loginradius.com before starting migration.
Profile Object
Each entry in the Profiles array represents one user. All fields are optional unless noted.
Note: The
Passwordfield (plaintext) is always discarded. UsePasswordHashto supply pre-hashed credentials.
Identity Fields
| Field | Type | Notes |
|---|---|---|
Email | Array of {Type, Value} | Required when app uses email or email+phone login. Value is automatically lowercased. |
UserName | String | Required when app uses username-primary login mode. |
PhoneId | String | Required when app uses phone-only login. |
PasswordHash | String | Pre-hashed password. Encoding must match PasswordHashEncodingType. |
ExternalUserLoginId | String | External system identifier. |
Duplicate email types are not supported. Each email type, such as
PrimaryorSecondary, should appear only once per profile. If multiple email entries have the sameType, only one email is retained. Do not send multiple emails with the same type, such as twoSecondaryemails.
Uid,IsDeleted,IsLoginLocked,AcceptPrivacyPolicy, andExternalIdsare automatically stripped. These fields cannot be set via migration.
Personal Information
| Field | Type |
|---|---|
FirstName | String |
LastName | String |
MiddleName | String |
NickName | String |
Prefix | String |
Suffix | String |
ProfileName | String |
Gender | String |
BirthDate | String |
About | String |
TagLine | String |
Company | String |
Industry | String |
FullNameis set automatically. We compose and save the profile's full name by joiningPrefix,FirstName,MiddleName,LastName, andSuffixwith spaces.
Contact & Location
| Field | Type |
|---|---|
PhoneNumbers | Array of {PhoneType, PhoneNumber} |
Addresses | Array of address objects |
MainAddress | String |
HomeTown | String |
City, State | String |
Country | Object: {Code, Name} |
TimeZone | String |
LocalLanguage | String |
Online Presence
| Field | Type |
|---|---|
Website | String |
ImageUrl | String |
ThumbnailImageUrl | String |
ProfileUrl | String |
ProfileImageUrls | Map of {String: String} |
WebProfiles | Map of {String: String} |
State & Flags
| Field | Type | Notes |
|---|---|---|
EmailVerified | Boolean | Whether the email address has been verified. |
PhoneIdVerified | Boolean | Whether the phone number has been verified. |
IsEmailSubscribed | Boolean | Marketing email opt-in status. |
Migration Date Fields
All dates must be parseable ISO 8601 strings. Dates in the past are accepted; future dates are rejected except for PasswordExpirationDate.
| Field | Type | Future Date Allowed |
|---|---|---|
CreatedDate | String (ISO 8601) | No |
ModifiedDate | String (ISO 8601) | No |
LastLoginDate | String (ISO 8601) | No |
LastPasswordChangeDate | String (ISO 8601) | No |
PasswordExpirationDate | String (ISO 8601) | Yes |
Login Statistics
| Field | Type | Notes |
|---|---|---|
NoOfLogins | Integer | Historical login count. Must be ≥ 0. |
Custom Data
| Field | Type | Notes |
|---|---|---|
CustomFields | Array of {String: String} | Arbitrary key-value pairs. Keys must be pre-registered in the tenant before migration. |
SecurityQuestionAnswer | Map of {String: String} | Security Q&A pairs as a key-value object (question ID → answer). |
To include
SecurityQuestionAnswerin bulk migration, the corresponding security questions must already exist in the tenant configuration (added via the Admin Console). Retrieve the Security Question IDs (used as the map keys) from the LoginRadius Configuration API.
Social Providers
Use SocialProviders to link external social identities to the migrated profile.
"SocialProviders": [
{
"Name": "google",
"Id": "google-user-id-123"
}
]
The Name field must be one of the supported social providers: facebook, google, apple, amazon, or linkedin. Any other value is rejected during per-record validation (see the "Invalid social provider" error below).
SocialProvidersare applied only when a profile is inserted (a new user). On an existing record matched during delta migration, submittedSocialProvidersare ignored.
B2B Fields
Only valid when the tenant is configured for B2B. If OrgRoles is provided, a primary email is required.
"RoleIds": ["role-id-1", "role-id-2"],
"OrgRoles": [
{
"OrgId": "org-abc123",
"RoleIds": ["member", "admin"]
}
]
Users with no roles specified are automatically assigned the tenant's DefaultMemberRole.
DeltaMigrationModel Object
Controls behavior when a submitted profile matches an existing record (matched on the tenant's configured unique fields: email, phone, username, or a combination).
Email Matching in Delta Mode
When the tenant matches on email and the incoming profile carries multiple Email entries, the email used to look up an existing (duplicate) record in the database is chosen from the request body by type, not array position:
- The incoming entry with
Type: "Primary"is used to find the matching record. - If the incoming profile has no
Primaryentry, the first entry in itsEmailarray is used as a fallback.
Because the lookup keys on the Primary email from the request rather than array order, a delta re-run produces the same result regardless of how the source export orders the Email array.
| Field | Type | Default | Description |
|---|---|---|---|
DeltaMigration | Boolean | false | false = insert-only; duplicates fail. true = upsert; duplicates are handled per OverWriteDuplicate. |
OverWriteDuplicate | Boolean | false | false = skip existing records. true = update existing records with submitted fields. |
Behavior Matrix
DeltaMigration | OverWriteDuplicate | Existing record | New record |
|---|---|---|---|
false | — | Fails (reported in Failed[]) | Inserted |
true | false | Skipped (no update) | Inserted |
true | true | Updated | Inserted |
Insert-Only Fields (Never Overwritten in Delta Mode)
When OverWriteDuplicate=true, the following fields on existing records are protected and never overwritten:
Uid,_id,AppId,Provider,RegistrationProviderIsActive,IsDeleted,IsLoginLocked,DisableLogin,IsProtectedCreatedDate,ModifiedDate,LastPasswordChangeDate,PasswordExpirationDate,LastLoginDateNoOfLogins,FailedLoginAttempt,FailedResetAttempt,TokenSignSecretSignupLog,PasswordHistory,B2BInfo,PrivacyPolicyHistory,PasskeyData,BreachedPassword
In delta mode, user roles (
OrgRoles,RoleIds) and social identities (SocialProviders) are not updated on existing records, they are only set during initial insert.
Response
HTTP 200 is returned for both full success and partial failures. Inspect Failed[] to identify rejected records.
{
"Profile": {
"RecordInserted": 498,
"RecordUpdated": 0,
"Failed": [
{
"record": 3,
"message": "E11000 duplicate key error: email already exists"
}
]
}
}
| Field | Type | Description |
|---|---|---|
Profile.RecordInserted | Integer | Count of successfully inserted records. |
Profile.RecordUpdated | Integer | Count of successfully updated records (delta mode only). |
Profile.Failed | Array | Per-record errors. record is 1-indexed. message contains the error detail. |
Error Codes
| Code | HTTP Status | Trigger |
|---|---|---|
7914 | 400 | Empty or malformed JSON body |
7992 | 400 | Invalid PasswordEncryption configuration (Type, SaltAttachType, encoding values, numeric ranges) |
8198 | 400 | One or more profiles failed field validation. Includes Errors[] array with per-record detail. |
8199 | 400 | Request contains more than 500 profiles |
8247 | 400 | Migration is not enabled for your tenant's login configuration (phone, username, or compound login). Contact support to enable it. |
8197 | 500 | Unrecoverable database error during bulk write |
401/403 | 401/403 | Missing credentials, invalid API key/secret, missing write:migrations scope, or IP not whitelisted |
Validation Error Response (8198)
{
"ErrorCode": 8198,
"Message": "One or more records have parameters that are not formatted correctly.",
"Description": "Some records in the request contain invalid or incorrectly formatted parameters. Please review the 'Errors' array for details on each affected record.",
"Errors": [
{
"FieldName": "Record 1, Field Email",
"ErrorMessage": "Email is required"
},
{
"FieldName": "Record 2, Field CreatedDate",
"ErrorMessage": "date cannot be in the future"
}
]
}
Migration Not Enabled (8247)
Returned when self-serve migration has not been provisioned for your tenant's login configuration.
{
"ErrorCode": 8247,
"Message": "Your account isn't ready for migration yet.",
"Description": "Migration isn't enabled for your login configuration (such as phone or username login). Please contact support and ask them to enable migration for your account."
}
Self-serve migration is enabled out of the box for email-based login. If your tenant uses phone, username, or a compound login mode (e.g. email + phone), migration must be enabled for your account before you begin.
If your tenant does not use standard email-based login, contact LoginRadius support before starting migration. Support will enable self-serve migration for your login configuration.
Per-Profile Validation Error Messages
| Error Message | Cause |
|---|---|
"Email is required" | App uses email login and no email provided |
"Phone is required" | App uses phone-only login and no PhoneId provided |
"Username is required" | App uses username-primary login and no UserName provided |
"invalid date format" | Date field cannot be parsed |
"date cannot be in the future" | CreatedDate, ModifiedDate, LastLoginDate, or LastPasswordChangeDate is a future timestamp |
"value cannot be negative" | NoOfLogins is negative |
"email format is invalid" | Server-side email validation is enabled and email fails the check |
"Organization ID not found" | OrgRoles[].OrgId does not exist in the tenant |
"Role ID not found" | A role ID in OrgRoles[].RoleIds or RoleIds[] does not exist |
"Invalid social provider" | SocialProviders[].Name is not one of the supported providers (facebook, google, apple, amazon, linkedin) |
Examples
Insert with PBKDF2 Password Hash
curl -X POST "https://<your-lr-domain>/v2/bulk/upsert" \
-H "X-LoginRadius-ApiKey: <apiKey>" \
-H "X-LoginRadius-ApiSecret: <apiSecret>" \
-H "Content-Type: application/json" \
-d '{
"PasswordEncryption": {
"Type": "pbkdf2",
"SaltAttachType": "Attach",
"IsPerPasswordSalt": false,
"PasswordHasherVersion": "V3",
"Salt": "c2FsdFZhbHVlMTIz",
"NumberOfIteration": 10000,
"SubKeyLength": 32,
"SaltKeyLength": 16,
"PasswordHashEncodingType": "base64",
"PasswordSaltEncodingType": "base64"
},
"Profiles": [
{
"Email": [{ "Type": "Primary", "Value": "john.doe@example.com" }],
"FirstName": "John",
"LastName": "Doe",
"PasswordHash": "<base64-encoded-hash>",
"PhoneId": "12025551234",
"BirthDate": "1990-01-15",
"Gender": "M",
"TimeZone": "America/New_York",
"LocalLanguage": "en",
"EmailVerified": true,
"CreatedDate": "2021-03-10T08:00:00Z",
"LastLoginDate": "2024-11-01T12:00:00Z",
"NoOfLogins": 42,
"Addresses": [
{
"Type": "Home",
"Address1": "123 Main St",
"City": "New York",
"State": "NY",
"PostalCode": "10001",
"Country": "US"
}
],
"CustomFields": {
"subscription_tier": "premium",
"registration_source": "web"
}
}
]
}'
Delta Upsert (Update Existing Users)
curl -X POST "https://<your-lr-domain>/v2/bulk/upsert" \
-H "X-LoginRadius-ApiKey: <apiKey>" \
-H "X-LoginRadius-ApiSecret: <apiSecret>" \
-H "Content-Type: application/json" \
-d '{
"Profiles": [
{
"Email": [{ "Type": "Primary", "Value": "existing.user@example.com" }],
"FirstName": "UpdatedName",
"PasswordHash": "<new-base64-encoded-hash>"
}
],
"DeltaMigrationModel": {
"DeltaMigration": true,
"OverWriteDuplicate": true
}
}'
Argon2id Password Hash
curl -X POST "https://<your-lr-domain>/v2/bulk/upsert" \
-H "X-LoginRadius-ApiKey: <apiKey>" \
-H "X-LoginRadius-ApiSecret: <apiSecret>" \
-H "Content-Type: application/json" \
-d '{
"PasswordEncryption": {
"Type": "argon2id",
"SaltAttachType": "Attach",
"IsPerPasswordSalt": true,
"NumberOfIteration": 3,
"SubKeyLength": 32,
"SaltKeyLength": 16,
"PasswordHashThread": 4,
"PasswordHashMemory": 4096,
"PasswordHashEncodingType": "base64"
},
"Profiles": [
{
"Email": [{ "Type": "Primary", "Value": "user@example.com" }],
"PasswordHash": "<argon2id-encoded-hash-with-embedded-salt>"
}
]
}'
Troubleshooting
Authentication Errors
- 401 / Invalid Credentials: Confirm your API Key and Secret are from the correct tenant environment (production vs sandbox).
- IP Not Whitelisted: If your tenant enforces IP allowlisting, add your migration server's IP.
Data Validation Failures
- Missing required field: Check which login mode the tenant is configured for (email, phone, username, or combo) and ensure every profile satisfies that requirement.
- Invalid date format: Use ISO 8601 format (
2023-01-15T10:30:00Zor2023-01-15). Timestamps must not be in the future (exceptPasswordExpirationDate). - Custom field not found: Pre-register all custom field keys in the Admin Console before migration.
Batch Size & Performance
- Too many records (
8199): Split into batches of ≤ 500 profiles. - Slow throughput: Run multiple batches concurrently. Each request processes independently with
ordered: false. - Partial failures (
Failed[]is non-empty): Each entry includes a 1-indexedrecordnumber and an errormessage. Correct those records and re-submit only the failed ones usingDeltaMigration: true, OverWriteDuplicate: falseto avoid re-inserting already-successful records.