For more than ~10 users at a time, bulk import is the right tool. You'll use it for:
- One-time migrations from another auth platform.
- Periodic syncs from your HR system (better long-term: SCIM, but bulk works for quarterly bulk catch-up).
- Loading test data into a staging tenant.
Two formats
Section titled “Two formats”CSV (recommended for human-driven imports)
Section titled “CSV (recommended for human-driven imports)”The familiar spreadsheet shape. The platform's CSV importer has a click-to-map step where you tell it which column is email, which is name, which is password, etc. Forgiving about extra columns.
Users → Bulk import → CSV. Upload; map; review; confirm.
The default expected columns are: email, name, given_name, family_name, password, email_verified, password_must_be_reset, groups (comma-separated group names), plus any custom-attribute columns matching your tenant's schema.
NDJSON (recommended for programmatic imports)
Section titled “NDJSON (recommended for programmatic imports)”One JSON object per line; one user per line. Streamed up to the platform; processed as it arrives. Better for large imports (tens of thousands) because nothing's buffered in memory.
Users → Bulk import → NDJSON. Paste or upload.
{"email":"anita@cymmetri.com","name":"Anita Singh","groups":["Engineering"]}{"email":"bob@cymmetri.com","name":"Bob Lee","groups":["Engineering","Beta Testers"]}{"email":"carol@cymmetri.com","name":"Carol Patel","custom_attributes":{"department":"Finance"}}Programmatic clients (a script piping output from your HR system) usually emit NDJSON directly.
Per-row fields
Section titled “Per-row fields”| Field | Required | Notes |
|---|---|---|
email | yes | Unique within the tenant. RFC 5321/5322 validated. |
name | no | Display name. Splits into given/family if you supply just name. |
given_name / family_name | no | Override the split if you supply them explicitly. |
password | no | If absent, the user is created without a password identity. They sign in via invitation or SSO. |
email_verified | no, defaults false | Set true if you trust the email source (e.g., migrating from a verified system). |
password_must_be_reset | no, defaults false | Set true if the password you supply is temporary. |
groups | no | Array of group names (existing groups; unknown names fail the row). |
custom_attributes | no | Map of arbitrary key-value pairs matching the tenant's custom-attribute schema. |
locale | no | IETF tag. Defaults to tenant default. |
Any field on the user record schema is acceptable in custom_attributes. Unknown top-level fields are rejected.
How the import runs
Section titled “How the import runs”- The platform parses the file. Validation errors at this stage stop the whole import (e.g., malformed JSON in NDJSON, missing required columns in CSV).
- If parsing succeeds, the platform creates a job and shows you a progress UI. You can leave the page; come back; the job runs in the background.
- Each row is applied atomically. If a row fails (e.g., email already exists, password fails policy, group not found), the row is skipped; the rest continue.
- The job's final state shows:
importedcount,failedcount,errorslist with row numbers + error messages.
Each successful row produces a user.created audit-log entry. The job itself emits user.bulk_import.started and user.bulk_import.completed (with summary counts).
Idempotency
Section titled “Idempotency”Re-running the same file is idempotent ONLY if every email is unique to that row. The default behaviour for "email already exists" is to fail the row — it's NOT an upsert. If you want upsert (update existing users in-place, create new ones), use the mode: upsert flag on the NDJSON header line:
{"_mode":"upsert"}{"email":"anita@cymmetri.com","name":"Anita Singh (updated)"}Or check the CSV importer's "upsert mode" checkbox before confirming.
In upsert mode, the row updates existing user fields in place. Caveat: you can't change a user's email via upsert (that's an identity-changing operation; use the per-user email edit flow).
Passwords in bulk import
Section titled “Passwords in bulk import”If your CSV / NDJSON includes passwords, those passwords are evaluated against the tenant's password policy. Rows whose passwords fail the policy are rejected with a clear error.
Important: passwords in bulk-import files are sensitive. Treat the file like a credentials list:
- Don't commit it to source control.
- Don't send it over chat / email.
- Delete it from your local disk after the import completes.
A safer pattern for large imports: omit passwords entirely; let users set their own via the welcome email triggered for each created user (configurable; default off).
Large imports
Section titled “Large imports”For more than ~10,000 users, the recommended pattern is NDJSON streamed via the management API rather than the UI. The UI handles up to ~50,000 in a single file but the browser tab needs to stay open for the duration. The API approach is fire-and-forget; you poll the job.
For tenants with very large existing user populations to migrate, see the bulk user migration runbook — covers preflight validation, staging tenant rehearsal, and the rollback path.
Common failure modes
Section titled “Common failure modes”- All rows fail with "email already exists" — you're re-importing without upsert mode. Add
_mode: upsertor filter the new rows. - Some rows fail with "group X not found" — the import doesn't auto-create groups. Pre-create the groups, then re-run.
- Password policy errors — sample a few of the failed passwords against the tenant policy in Settings → Password policy; adjust either the policy or the password generation.
- Custom attribute schema mismatch — unknown attributes are rejected. Either add them to the tenant's custom-attribute schema, or remove them from the import.