Skip to main content

Bulk importing users

The CSV and JSON import formats, the exact validation errors, and what happens to the rows after a failure. Fixed since it last mattered: an import now attaches a real, working membership.

Written for whoever runs IT8 min readUpdated

Qorionix can create many user accounts in one request, from a CSV file or from JSON. This article documents the format precisely, because an import that half-works is worse than one that fails cleanly and you will want to know which you got.

For a single person, Who actually needs a Qorionix account and the Invite dialog remain the more direct path when you only need to add one or two people — though see What an invited user actually receives before you rely on it, since inviting somebody today does not email them anything. A non-invited bulk import, by contrast, sets a real password yourself and produces an account that works immediately with no delivery step at all — see the note on invited: false below.

Who can run an import

Both import endpoints require the users.write permission, which of the built-in roles only admin holds. A member or viewer attempting an import receives:

insufficient permissions
HTTP 403

That is the standard denial across the whole platform. The permission model explains where it comes from and how to tell which permission was missing.

The CSV format

The file is sent as the raw body of the request. It is not a multipart file upload, which matters if you are scripting it — a tool that wraps the file in a form will produce a header row that is not a header row.

  • The first line must be a header row. Columns are located by name, not by position, so the order is irrelevant.
  • Column names are matched case-sensitively. Email is not email. A file exported from a spreadsheet with capitalised headings will be rejected.
  • email and password are required. first_name and last_name are optional and are simply left blank if the column is absent.
  • Any other column is ignored. You can leave a department or an employee number in the file; nothing reads it.
A correct import file
email,password,first_name,last_name
[email protected],Xq7!vetRoad2Loop,Anna,Kowalska
[email protected],Bramble#94Kestrel,Tom,Byrne
[email protected],Trellis!Harbour38,Sade,Okafor
[email protected],Quill4?Lantern-Bay,Payroll,Review

Column order does not matter, so this file is equally valid and imports the same four people:

The same import, columns reordered, optional columns dropped
password,email
Xq7!vetRoad2Loop,[email protected]
Bramble#94Kestrel,[email protected]
Trellis!Harbour38,[email protected]
Quill4?Lantern-Bay,[email protected]

The JSON format

The JSON endpoint takes an array of the same four fields, plus one flag.

The JSON body
{
  "users": [
    { "email": "[email protected]",
      "password": "Xq7!vetRoad2Loop",
      "first_name": "Anna", "last_name": "Kowalska" }
  ],
  "invited": false
}

Setting invited to true creates each account in the STAGED state with an unguessable placeholder password, so the person must set their own before they can sign in. That is the safer of the two shapes. It comes with a wrinkle worth naming: the row is still rejected if the password field is empty, even though an invited account never uses the value. Put a throwaway string in the field. It is discarded.

Row limits

One thousand users per batch, but the two endpoints enforce it differently and the difference is dangerous.

How each endpoint handles a batch of more than 1,000 rows.
EndpointBehaviour above 1,000 rows
JSONThe whole request is rejected with maximum 1000 users per batch and HTTP 400. Nothing is created. This is the behaviour you want.
CSVThe file is silently truncated. The first 1,000 rows are imported, rows 1,001 onwards are discarded, and the response says nothing about it. A 1,400-row file reports 1,000 created and looks like a complete success.

If you are importing more than a thousand people from CSV, split the file yourself and reconcile the counts. There is no size limit on the request body, only a row limit, so a large file will be accepted and quietly cut.

The exact error messages

Two kinds of error exist and they behave differently. A request-level error rejects the whole batch. A row-level error skips one row and lets the rest through.

Request-level errors. Each is returned as HTTP 400 and nothing is created.
MessageWhich endpointCause
invalid bodyJSONThe body is not valid JSON, or the field types are wrong.
users array is requiredJSONThe users array is missing, or present but empty.
maximum 1000 users per batchJSONMore than 1,000 entries.
failed to read CSV headerCSVThe first line could not be parsed — usually an empty body, or a file that is not CSV at all.
CSV must contain email and password columnsCSVThe header row does not contain both names, spelled exactly in lower case.

Row-level errors are collected into an errors array in the response and do not stop the batch. The import is partial-success: good rows are created and stay created even when other rows fail. There is no transaction and no rollback, so a half-imported file leaves half the accounts in place.

Row-level error strings, as they appear in the response's errors array.
MessageMeaning
missing email or password for entryThe row had an empty email or an empty password. It does not tell you which row. There is no row number and no email address in the message, so with three of these in a 400-row file you are on your own finding them.
hash failed for <email>: <error>The password could not be hashed. Rare; usually means an extremely long password value.
<email>: <error>The account could not be created. The most common cause by a distance is a duplicate address, which produces <email>: user email <email>: already exists — the address really does appear twice in the one message.

A successful call always returns HTTP 200, whatever the mix of successes and failures, with this shape:

The response
{
  "created": 47,
  "failed": 3,
  "errors": ["[email protected]: user email [email protected]: already exists"],
  "user_ids": ["..."]
}

Duplicates

Email addresses are unique across the platform. A row whose address already belongs to an account fails that one row and leaves the existing account completely untouched — nothing is overwritten, no password is reset, no name is changed. Re-running the same file is therefore safe in the narrow sense that it will not damage what already exists; it will simply report every row as failed.

Duplicates within one file behave the same way: the first occurrence is created, the second fails against the first. Order in the file decides which one wins, and the file is processed top to bottom.

What you cannot set through an import

Several fields are fixed by the importer and cannot be supplied in the file. Every account created by a non-invited import is active, marked as having a verified email address, typed as EMPLOYEE, given the status ACTIVE, and assigned the fixed role member — every imported user gets the same role, whatever the file says. You cannot set a different role, a group, a profile attribute or an organisation through the import. If you need any of those, the import will not save you the work — you will be editing every account afterwards anyway.

What to do next

  1. What an invited user actually receives — read this before choosing Invite over a one-row import for a single person.
  2. Users and the directory — what a user record holds.
  3. Groups and automatic membership — how to organise people once they are in.

Was this article wrong?

If a procedure here does not match what you see, or a limit we described has changed, tell us and we will fix the page. Email us about this article, or see how to get help if you need an answer rather than a correction.

Everything in identity and access