Skip to content

Bulk import users

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

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.

FieldRequiredNotes
emailyesUnique within the tenant. RFC 5321/5322 validated.
namenoDisplay name. Splits into given/family if you supply just name.
given_name / family_namenoOverride the split if you supply them explicitly.
passwordnoIf absent, the user is created without a password identity. They sign in via invitation or SSO.
email_verifiedno, defaults falseSet true if you trust the email source (e.g., migrating from a verified system).
password_must_be_resetno, defaults falseSet true if the password you supply is temporary.
groupsnoArray of group names (existing groups; unknown names fail the row).
custom_attributesnoMap of arbitrary key-value pairs matching the tenant's custom-attribute schema.
localenoIETF tag. Defaults to tenant default.

Any field on the user record schema is acceptable in custom_attributes. Unknown top-level fields are rejected.

  1. 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).
  2. 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.
  3. 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.
  4. The job's final state shows: imported count, failed count, errors list 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).

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

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

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.

  • All rows fail with "email already exists" — you're re-importing without upsert mode. Add _mode: upsert or 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.