API Reference

Users

A user account is what a person signs in to your deployment with. An administrator creates accounts one at a time with a password, in bulk, or by importing them from Microsoft Entra ID (see Directory synchronisation). Where the deployment allows it, people who sign in with Microsoft for the first time are also given an account automatically.

An account signs in with its password (auth_source is local) or with Microsoft Entra ID (entra). For an entra account, the name, email address and password can't be changed through this API, and its role can be changed only once the role is pinned with role_locked.

Accounts are never removed through this API. Deactivate a user keeps the account and its data and stops the user signing in; Update a user reactivates it. The last active administrator who can sign in with a password can't be deactivated, so that somebody can still sign in if Microsoft sign-in becomes unavailable.

Administrator access. Every endpoint on this page requires an administrator account.

The user object

Retrieve, create and update return every field below except those marked as list results only. List users returns those fields instead of email_verified and email_verified_at.

Attributes

  • id integer

    Unique identifier for the user.

  • email string

    The user's email address.

  • name string

    The user's display name.

  • user_type string

    The user's role. The possible values are listed under user_type in Create a user.

  • is_active boolean

    Whether the account is active. A deactivated account can't sign in.

  • is_superadmin boolean

    Whether the user is an administrator.

  • auth_source string

    How the user signs in: local with a password, or entra with Microsoft Entra ID. List results only.

  • role_locked boolean

    Whether the role is pinned, so that signing in with Microsoft doesn't change it through directory group role mappings. List results only.

  • last_role_sync_at nullable string

    When the role was last checked against directory group role mappings at a Microsoft sign-in, or null if it never has been. List results only.

  • email_verified boolean

    Whether the email address is marked as verified. Accounts created by an administrator are created verified. Not included in list results.

  • email_verified_at nullable string

    When the email address was marked as verified. Not included in list results.

  • created_at string

    When the account was created, in ISO 8601 format.

  • updated_at string

    When the account was last changed, in ISO 8601 format. List results only.

The user object
{
  "id": 42,
  "email": "ada@example.com",
  "name": "Ada Lovelace",
  "user_type": "default",
  "is_active": true,
  "is_superadmin": false,
  "email_verified": true,
  "email_verified_at": "2026-09-01T09:30:00.412893+00:00",
  "created_at": "2026-09-01T09:30:00.412893+00:00"
}

List users

GET /api/admin/users

Returns a page of user accounts, newest first, with optional filters.

Returns the deployment's accounts, including deactivated ones, ordered by creation time with the newest first. Filters are optional and combine. The response gives total (the number of matching accounts), page, limit, and pages (the number of pages at this limit).

Authentication
Bearer token How it works
Permission
Requires an administrator account.

Query parameters

  • page integer

    The page to return, counting from 1.

    Defaults to 1.

  • limit integer

    Accounts per page. Values above 100 are treated as 100.

    Defaults to 50.

  • email string

    Text to look for, case-insensitively, in the email address or the name.

  • user_type string

    Only accounts with this role, for example default.

  • auth_source string

    Only accounts that sign in this way: local or entra.

  • is_active boolean

    true returns active accounts only; any other value, such as false, returns deactivated accounts only.

  • date_from string

    Only accounts created on or after this date, as YYYY-MM-DD. An invalid date is ignored.

  • date_to string

    Only accounts created up to the start (00:00) of this date, as YYYY-MM-DD. To include the whole day, pass the next day. An invalid date is ignored.

Returns

Returns a page of user objects and the pagination details.

Request
curl "$VDF_BASE_URL/api/admin/users?page=1&limit=50" \
  -H "Authorization: Bearer $VDF_ACCESS_TOKEN"
Response 200
{
  "success": true,
  "users": [
    {
      "id": 43,
      "email": "grace@example.com",
      "name": "Grace Hopper",
      "user_type": "expert",
      "is_active": true,
      "is_superadmin": false,
      "auth_source": "entra",
      "role_locked": false,
      "last_role_sync_at": "2026-09-03T07:58:41.905126+00:00",
      "created_at": "2026-09-02T08:15:27.118402+00:00",
      "updated_at": "2026-09-02T08:15:27.118402+00:00"
    },
    {
      "id": 42,
      "email": "ada@example.com",
      "name": "Ada Lovelace",
      "user_type": "default",
      "is_active": true,
      "is_superadmin": false,
      "auth_source": "local",
      "role_locked": false,
      "last_role_sync_at": null,
      "created_at": "2026-09-01T09:30:00.412893+00:00",
      "updated_at": "2026-09-01T09:30:00.412893+00:00"
    }
  ],
  "total": 2,
  "page": 1,
  "limit": 50,
  "pages": 1
}

Create a user

POST /api/admin/users

Creates a user account that signs in with a password.

Creates an account that signs in with the password you set. The email address is stored in lower case. Accounts created by an administrator are marked as verified (email_verified is true), so there is no confirmation step.

The new user is sent a welcome email that doesn't contain the password, so share the password with them separately. A welcome email that can't be sent doesn't fail the request.

Creating an active account needs a free licence seat where your deployment enforces a limit on active users.

Authentication
Bearer token How it works
Permission
Requires an administrator account.

Body parameters application/json

  • email string Required

    The user's email address, at most 120 characters. It must not already belong to an account, in any letter case.

  • password string Required

    The user's password: at least 6 characters and at most 72 bytes.

  • name string

    The user's display name, at most 120 characters. Defaults to the part of the email address before the @.

  • user_type string

    The user's role. Assigning the administrator role makes the user an administrator, with is_superadmin set to true. admin is an ordinary role and doesn't give access to administration endpoints.

    Defaults to default.

    Possible values
    • default
    • explorative
    • expert
    • admin
    • superadmin
  • is_active boolean

    Set to false to create the account deactivated. A deactivated account can't sign in and doesn't use a licence seat.

    Defaults to true.

Returns

Returns the new user object.

Errors

  • 400 email is missing, longer than 120 characters or not a valid address; name is longer than 120 characters; password is missing, shorter than 6 characters or longer than 72 bytes; or user_type is not a known role.
  • 402 The licence doesn't currently allow adding users.
  • 403 Your deployment enforces a limit on active users and no seats remain. The response's error is USER_LIMIT_REACHED, and seats gives the numbers.
  • 409 An account with this email address already exists.
Request
curl -X POST "$VDF_BASE_URL/api/admin/users" \
  -H "Authorization: Bearer $VDF_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "katherine@example.com",
    "name": "Katherine Johnson",
    "password": "<PASSWORD>",
    "user_type": "expert"
  }'
Response 201
{
  "success": true,
  "user": {
    "id": 44,
    "email": "katherine@example.com",
    "name": "Katherine Johnson",
    "user_type": "expert",
    "is_active": true,
    "is_superadmin": false,
    "email_verified": true,
    "email_verified_at": "2026-09-04T10:12:45.203117+00:00",
    "created_at": "2026-09-04T10:12:45.203117+00:00"
  },
  "message": "User created successfully"
}

Retrieve a user

GET /api/admin/users/{user_id}

Returns one user account.

Returns the account with this ID, whether it is active or deactivated.

Authentication
Bearer token How it works
Permission
Requires an administrator account.

Path parameters

  • user_id integer Required

    The user's ID.

Returns

Returns the user object.

Errors

  • 404 No account has this ID.
Request
curl "$VDF_BASE_URL/api/admin/users/42" \
  -H "Authorization: Bearer $VDF_ACCESS_TOKEN"
Response 200
{
  "success": true,
  "user": {
    "id": 42,
    "email": "ada@example.com",
    "name": "Ada Lovelace",
    "user_type": "default",
    "is_active": true,
    "is_superadmin": false,
    "email_verified": true,
    "email_verified_at": "2026-09-01T09:30:00.412893+00:00",
    "created_at": "2026-09-01T09:30:00.412893+00:00"
  }
}

Update a user

PUT /api/admin/users/{user_id}

Changes a user's details, role, password or active state.

Changes only the fields you send. Reactivating a deactivated account (setting is_active to true) needs a free licence seat where your deployment enforces a limit on active users. The request is refused if it would deactivate the last active administrator who can sign in with a password.

For an account that signs in with Microsoft Entra ID (auth_source is entra), the request is refused if it sets name, email or password. Its role can be changed only when the role is pinned, with role_locked set to true in the same request or already; otherwise the directory group role mappings would set the role again at the user's next Microsoft sign-in.

Authentication
Bearer token How it works
Permission
Requires an administrator account.

Path parameters

  • user_id integer Required

    The user's ID.

Body parameters application/json

  • name string

    The new display name.

  • email string

    The new email address. It must not belong to another account.

  • password string

    A new password. An empty value leaves the password unchanged.

  • user_type string

    The new role, written exactly as one of the values below. Assigning the administrator role makes the user an administrator; assigning any other role removes administrator access.

    Possible values
    • default
    • explorative
    • expert
    • admin
    • superadmin
  • is_active boolean

    false deactivates the account; true reactivates it.

  • role_locked boolean

    true pins the role, so that signing in with Microsoft doesn't change it through directory group role mappings.

Returns

Returns the updated user object.

Errors

  • 400 user_type is not exactly one of the listed roles.
  • 402 Reactivation was refused because the licence doesn't currently allow adding users.
  • 403 Reactivation was refused because your deployment enforces a limit on active users and no seats remain. The response's error is USER_LIMIT_REACHED.
  • 404 No account has this ID.
  • 409 Another account uses this email address; the account signs in with Microsoft Entra ID and the request sets name, email or password, or changes its role without pinning it; or the request would deactivate the last active administrator who can sign in with a password.
Request
curl -X PUT "$VDF_BASE_URL/api/admin/users/42" \
  -H "Authorization: Bearer $VDF_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "user_type": "superadmin"
  }'
Response 200
{
  "success": true,
  "user": {
    "id": 42,
    "email": "ada@example.com",
    "name": "Ada Lovelace",
    "user_type": "superadmin",
    "is_active": true,
    "is_superadmin": true,
    "email_verified": true,
    "email_verified_at": "2026-09-01T09:30:00.412893+00:00",
    "created_at": "2026-09-01T09:30:00.412893+00:00"
  },
  "message": "User updated successfully"
}

Deactivate a user

DEL /api/admin/users/{user_id}

Deactivates a user account without deleting it.

Sets the account's is_active to false. The account, its data, its role and its group memberships are kept, and the user can no longer sign in. A deactivated account doesn't count towards the licence's active users. Reactivate it with Update a user.

The last active administrator who can sign in with a password can't be deactivated.

Authentication
Bearer token How it works
Permission
Requires an administrator account.

Path parameters

  • user_id integer Required

    The user's ID.

Returns

Returns a success marker.

Errors

  • 404 No account has this ID.
  • 409 The account is the last active administrator who can sign in with a password.
Request
curl -X DELETE "$VDF_BASE_URL/api/admin/users/42" \
  -H "Authorization: Bearer $VDF_ACCESS_TOKEN"
Response 200
{
  "success": true,
  "message": "User deleted successfully"
}

Send a password reset email

POST /api/admin/users/{user_id}/resend-password

Sends the user an email with a link to set a new password. The link expires after 48 hours and can be used once. Use it when a user has forgotten their password, or to send again an invitation that didn't arrive.

Authentication
Bearer token How it works
Permission
Requires an administrator account.

Path parameters

  • user_id integer Required

    The user's ID.

Returns

Returns a confirmation naming the address the email was sent to.

Errors

  • 404 No account has this ID.
  • 500 The email couldn't be sent, for example because outgoing email isn't configured.
Request
curl -X POST "$VDF_BASE_URL/api/admin/users/42/resend-password" \
  -H "Authorization: Bearer $VDF_ACCESS_TOKEN"
Response 200
{
  "success": true,
  "message": "Password reset email sent to ada@example.com"
}

Retrieve bulk creation limits

GET /api/admin/users/bulk/preflight

Returns the free licence seats, whether invitation emails can be sent, and the most rows a batch may contain.

Call this before Create users in bulk to size a batch.

  • seats is the licence position: maxUsers is the limit on active users (null when there is none), activeUsers the number of active accounts, seatsRemaining the free seats (null when there is no limit), and enforced whether a limit applies.
  • emailConfigured says whether outgoing email is set up, which invitation emails need. When it is false, emailIssue describes what is missing.
  • maxRows is the largest number of rows a batch may contain.
Authentication
Bearer token How it works
Permission
Requires an administrator account.

Parameters

No parameters.

Returns

Returns the seat position, the email readiness and the row limit.

Errors

  • 402 The licence doesn't currently allow adding users.
Request
curl "$VDF_BASE_URL/api/admin/users/bulk/preflight" \
  -H "Authorization: Bearer $VDF_ACCESS_TOKEN"
Response 200
{
  "success": true,
  "seats": {
    "maxUsers": 50,
    "activeUsers": 12,
    "seatsRemaining": 38,
    "enforced": true
  },
  "emailConfigured": true,
  "emailIssue": null,
  "maxRows": 500
}

Create users in bulk

POST /api/admin/users/bulk

Validates a batch of up to 500 accounts and, unless it is a dry run, creates them together.

Validates every row, then creates the accounts in a single transaction: if the batch fails, none of them are created. Set dry_run to see the outcome without creating anything. A bad row doesn't fail the batch; it is reported as invalid with a reason, and the other rows go ahead.

Each row gets a status:

  • create: a new account is created (in a dry run, would be created). It is active, signs in with a password and has its email address marked as verified. A row without a password creates the account without one, and the user sets it from the invitation email described below.
  • existing: an account with this email address already exists. The account is left as it is, except that it is added to the row's groups.
  • invalid: the row can't be used, and reason says why. A row with the same email address as an earlier row is invalid.

When the licence limits active users, the whole batch must fit in the free seats: otherwise nothing is created and the request fails with 403. A dry run reports the shortfall in seatShortfall instead.

After the accounts are created, and unless send_invites is false, each new user is emailed: a welcome email when the row had a password, or a link to set a password, valid for 48 hours, when it didn't. Each row's inviteStatus reports the outcome: sent, failed, not_configured when outgoing email isn't set up, or skipped when sending stopped after three failures in a row. Send a missing email later with Send a password reset email.

Authentication
Bearer token How it works
Permission
Requires an administrator account.

Body parameters application/json

  • rows array of objects Required

    The accounts to create, from 1 to 500 rows.

    Show child parameters Hide child parameters
    • email string

      The user's email address, at most 120 characters. A row without a valid address is invalid.

    • name string

      The user's display name, at most 120 characters. Defaults to the part of the email address before the @, with a warning.

    • password string

      An initial password: at least 6 characters and at most 72 bytes. Omit it to email the user a link to choose their own.

    • role string

      The account's role, from the values listed under user_type in Create a user. Defaults to default_role. The administrator role can't be assigned in bulk; a row that asks for it is invalid.

    • groups string

      Permission groups to join, by slug or name in any letter case, separated by ; or |. An array of names is also accepted. Unknown and inactive groups are skipped with a warning.

  • default_role string

    The role for rows that don't name one. The administrator role isn't allowed. Also accepted as defaultRole.

    Defaults to default.

  • group_ids array of integers

    IDs of permission groups that every usable row joins, including rows for existing accounts. Each group must exist and be active. Also accepted as groupIds.

  • dry_run boolean

    Validate and report without creating accounts, adding group members or sending email. Also accepted as dryRun.

    Defaults to false.

  • send_invites boolean

    Email each new user once the accounts are created. Also accepted as sendInvites.

    Defaults to true.

Returns

Returns the outcome of the batch and of each row.

Errors

  • 400 rows is missing, empty, longer than 500 rows or contains something other than objects (the response includes maxRows); default_role is unknown or the administrator role; group_ids isn't a list of integers; or a group in group_ids is inactive.
  • 402 The licence doesn't currently allow adding users. Dry runs fail in the same way.
  • 403 The licence limits active users and the batch doesn't fit in the free seats. Nothing was created; the response's error is USER_LIMIT_REACHED, and seats gives the numbers.
  • 404 A group in group_ids doesn't exist.
Request
curl -X POST "$VDF_BASE_URL/api/admin/users/bulk" \
  -H "Authorization: Bearer $VDF_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "rows": [
      {
        "email": "dorothy@example.com",
        "name": "Dorothy Vaughan",
        "role": "expert",
        "groups": "engineering; research"
      },
      {
        "email": "ada@example.com",
        "name": "Ada Lovelace"
      },
      {
        "email": "not-an-email"
      }
    ],
    "default_role": "default",
    "group_ids": [
      7
    ]
  }'
Response 200
{
  "success": true,
  "dryRun": false,
  "counts": {
    "total": 3,
    "create": 1,
    "existing": 1,
    "invalid": 1,
    "warnings": 1
  },
  "seats": {
    "maxUsers": 50,
    "activeUsers": 12,
    "seatsRemaining": 38,
    "enforced": true
  },
  "seatShortfall": 0,
  "emailConfigured": true,
  "groups": [
    {
      "id": 7,
      "slug": "engineering",
      "name": "Engineering",
      "added": 1
    }
  ],
  "rows": [
    {
      "index": 0,
      "email": "dorothy@example.com",
      "name": "Dorothy Vaughan",
      "role": "expert",
      "status": "create",
      "reason": null,
      "warnings": [
        "unknown group 'research'"
      ],
      "groups": [
        {
          "id": 7,
          "slug": "engineering",
          "name": "Engineering"
        }
      ],
      "unknownGroups": [
        "research"
      ],
      "userId": 45,
      "invite": "set-password",
      "inviteStatus": "sent",
      "inviteError": null
    },
    {
      "index": 1,
      "email": "ada@example.com",
      "name": "Ada Lovelace",
      "role": "default",
      "status": "existing",
      "reason": "existing account left unchanged",
      "warnings": [],
      "groups": [
        {
          "id": 7,
          "slug": "engineering",
          "name": "Engineering"
        }
      ],
      "unknownGroups": [],
      "userId": 42,
      "invite": "none",
      "inviteStatus": null,
      "inviteError": null
    },
    {
      "index": 2,
      "email": "not-an-email",
      "name": "",
      "role": "",
      "status": "invalid",
      "reason": "invalid email address",
      "warnings": [],
      "groups": [],
      "unknownGroups": [],
      "userId": null,
      "invite": "none",
      "inviteStatus": null,
      "inviteError": null
    }
  ]
}