API Reference

Directory synchronisation

These endpoints read your organisation's Microsoft Entra ID directory and turn what they find into VDF AI user accounts and permission groups. They use the directory connection configured by an administrator; Retrieve directory status reports whether it is ready.

An import never changes an existing account. A directory user who already has an account, either linked to the same directory identity or with the same email address, is reported as existing. New accounts sign in with Microsoft (auth_source is entra) and have no password, and creating them uses licence seats.

The import and sync endpoints accept dry_run, which reports what would change without writing anything. They read at most 999 direct user members of each directory group; members of nested groups aren't included.

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

The directory user object

A user in your Microsoft Entra ID directory. The import endpoints return the same fields plus userId, the ID of the matching account, or reason for a user they skipped.

Attributes

  • id string

    The user's object ID in the directory.

  • displayName nullable string

    The user's display name.

  • email string

    The user's mail address, or their user principal name when they have none, in lower case. Empty when the directory has neither.

  • userPrincipalName nullable string

    The user's sign-in name in the directory.

  • accountEnabled boolean

    Whether the account is enabled in the directory.

The directory user object
{
  "id": "9d3a7c2e-4b1f-4e8a-a6c5-2f7e1b0d8c34",
  "displayName": "Grace Hopper",
  "email": "grace@example.com",
  "userPrincipalName": "grace@example.com",
  "accountEnabled": true
}

Retrieve directory status

GET /api/admin/directory/entra/status

Reports whether the deployment can read your Microsoft Entra ID directory.

Checks the directory connection by listing a user and a group. It always responds with 200, so read ready.

  • When ready is true, tenant describes the directory: its id, displayName and verifiedDomains. displayName is null and verifiedDomains is empty when the deployment isn't allowed to read the organisation's details.
  • When ready is false, error describes the problem, and remediable is true when an administrator can fix it in Microsoft Entra ID.
Authentication
Bearer token How it works
Permission
Requires an administrator account.

Parameters

No parameters.

Returns

Returns whether the directory can be read and, if so, which directory it is.

Request
curl "$VDF_BASE_URL/api/admin/directory/entra/status" \
  -H "Authorization: Bearer $VDF_ACCESS_TOKEN"
Response 200
{
  "success": true,
  "ready": true,
  "tenant": {
    "id": "00000000-0000-0000-0000-000000000000",
    "displayName": "Contoso",
    "verifiedDomains": [
      "example.com"
    ]
  }
}

List directory users

GET /api/admin/directory/entra/users

Returns users from your Microsoft Entra ID directory.

Returns directory users, in the order the directory lists them, for choosing whom to bring in with Import directory users. Accounts disabled in the directory are included, with accountEnabled set to false.

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

Query parameters

  • search string

    Only users whose display name, user principal name or mail address starts with this text.

  • limit integer

    The most users to return, from 1 to 500.

    Defaults to 100.

Returns

Returns a list of directory user objects.

Errors

  • 400 The directory connection isn't configured. remediable is true.
  • 403 The directory refused the request because the directory connection lacks a permission it needs. remediable is true.
  • 429 The directory is limiting the rate of requests. Try again shortly.
  • 502 The directory couldn't be reached or returned an error.
Request
curl "$VDF_BASE_URL/api/admin/directory/entra/users?limit=100" \
  -H "Authorization: Bearer $VDF_ACCESS_TOKEN"
Response 200
{
  "success": true,
  "users": [
    {
      "id": "9d3a7c2e-4b1f-4e8a-a6c5-2f7e1b0d8c34",
      "displayName": "Grace Hopper",
      "email": "grace@example.com",
      "userPrincipalName": "grace@example.com",
      "accountEnabled": true
    },
    {
      "id": "c4f6a8b0-2d1e-4f3a-9b5c-7e0d2a4c6f81",
      "displayName": "Charles Babbage",
      "email": "charles@example.com",
      "userPrincipalName": "charles@example.com",
      "accountEnabled": false
    }
  ]
}

List directory groups

GET /api/admin/directory/entra/groups

Returns groups from your Microsoft Entra ID directory.

Returns directory groups, in the order the directory lists them, for choosing which to bring in with Import directory groups. Groups of every type are included; securityEnabled is true for security groups.

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

Query parameters

  • search string

    Only groups whose display name starts with this text.

  • limit integer

    The most groups to return, from 1 to 500.

    Defaults to 100.

Returns

Returns a list of directory groups, each with its object id, displayName, description, securityEnabled and mailNickname.

Errors

  • 400 The directory connection isn't configured. remediable is true.
  • 403 The directory refused the request because the directory connection lacks a permission it needs. remediable is true.
  • 429 The directory is limiting the rate of requests. Try again shortly.
  • 502 The directory couldn't be reached or returned an error.
Request
curl "$VDF_BASE_URL/api/admin/directory/entra/groups?search=Eng" \
  -H "Authorization: Bearer $VDF_ACCESS_TOKEN"
Response 200
{
  "success": true,
  "groups": [
    {
      "id": "3f2b8c1e-5a4d-4e6f-9b7a-1c2d3e4f5a6b",
      "displayName": "Engineering",
      "description": "Engineering staff",
      "securityEnabled": true,
      "mailNickname": "engineering"
    }
  ]
}

List directory group members

GET /api/admin/directory/entra/groups/{group_id}/members

Returns the user members of a directory group.

Returns the group's direct user members. Members of nested groups, and members that aren't users, aren't included.

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

Path parameters

Query parameters

  • limit integer

    The most members to return, from 1 to 500.

    Defaults to 100.

Returns

Returns a list of directory user objects.

Errors

  • 400 The directory connection isn't configured. remediable is true.
  • 403 The directory refused the request because the directory connection lacks a permission it needs. remediable is true.
  • 429 The directory is limiting the rate of requests. Try again shortly.
  • 502 The directory couldn't be reached or returned an error, for example because no group has this ID.
Request
curl "$VDF_BASE_URL/api/admin/directory/entra/groups/3f2b8c1e-5a4d-4e6f-9b7a-1c2d3e4f5a6b/members" \
  -H "Authorization: Bearer $VDF_ACCESS_TOKEN"
Response 200
{
  "success": true,
  "members": [
    {
      "id": "9d3a7c2e-4b1f-4e8a-a6c5-2f7e1b0d8c34",
      "displayName": "Grace Hopper",
      "email": "grace@example.com",
      "userPrincipalName": "grace@example.com",
      "accountEnabled": true
    },
    {
      "id": "c4f6a8b0-2d1e-4f3a-9b5c-7e0d2a4c6f81",
      "displayName": "Charles Babbage",
      "email": "charles@example.com",
      "userPrincipalName": "charles@example.com",
      "accountEnabled": false
    }
  ]
}

Import directory users

POST /api/admin/directory/entra/import/users

Creates accounts for the directory users you select.

Brings the selected directory users into VDF AI. Each ID is read again from the directory, so the import uses the directory's current details.

  • A user who already has an account is listed under existing and left unchanged.
  • A user whose account is disabled in the directory, or who has no email address there, is listed under skipped with a reason.
  • Everyone else gets a new account, listed under created. It signs in with Microsoft, has no password, takes its name and email address from the directory, and has the role default_role.

IDs the directory no longer knows are returned in unresolved. With group_ids, every created and existing user also joins those permission groups as a local member, so a sync doesn't remove them. The response also gives the role used for new accounts, how many members each group added, and seats, the licence position before the import.

The import is a single transaction. When the licence limits active users and the new accounts don't all fit, it stops and nothing is created. With dry_run, nothing is written: created lists the users who would get an account, with userId set to null, and seats is null if the licence doesn't currently allow adding users.

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

Body parameters application/json

  • object_ids array of strings Required

    Object IDs of the directory users to import, as returned by List directory users. At most 500. Also accepted as objectIds.

  • default_role string

    The role for new accounts, from the values listed under user_type in Create a user. An unrecognised value is treated as default. When omitted, new accounts get the role your deployment gives new Microsoft sign-ins. Also accepted as defaultRole.

  • group_ids array of integers

    IDs of permission groups that every created and existing user joins. Each group must exist and be active. Also accepted as groupIds.

  • dry_run boolean

    Report what would happen without creating accounts or memberships. Also accepted as dryRun.

    Defaults to false.

Returns

Returns the users created, found and skipped, the group assignments and the seat position.

Errors

  • 400 object_ids is missing or empty, or lists more than 500 IDs (the response includes maxSelection); group_ids isn't a list of integers; a group in group_ids is inactive; or the directory connection isn't configured.
  • 402 The licence doesn't currently allow adding users. Dry runs aren't affected.
  • 403 The licence limits active users and the new accounts don't fit in the free seats, so nothing was imported (the response's error is USER_LIMIT_REACHED); or the directory refused the request because the directory connection lacks a permission it needs.
  • 404 None of the IDs exist in the directory any more (they are listed in unresolved), or a group in group_ids doesn't exist.
  • 429 The directory is limiting the rate of requests. Try again shortly.
  • 502 The directory couldn't be reached or returned an error.
Request
curl -X POST "$VDF_BASE_URL/api/admin/directory/entra/import/users" \
  -H "Authorization: Bearer $VDF_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "object_ids": [
      "e2a4c6d8-1f3b-4d5e-8a7c-9b0d2f4e6a13",
      "5e8b1d4f-7a2c-4c9e-b3d6-8a1f0e2c7b95"
    ],
    "default_role": "default",
    "group_ids": [
      8
    ]
  }'
Response 200
{
  "success": true,
  "dryRun": false,
  "role": "default",
  "created": [
    {
      "id": "e2a4c6d8-1f3b-4d5e-8a7c-9b0d2f4e6a13",
      "displayName": "Mary Jackson",
      "email": "mary@example.com",
      "userPrincipalName": "mary@example.com",
      "accountEnabled": true,
      "userId": 47
    }
  ],
  "existing": [
    {
      "id": "5e8b1d4f-7a2c-4c9e-b3d6-8a1f0e2c7b95",
      "displayName": "Ada Lovelace",
      "email": "ada@example.com",
      "userPrincipalName": "ada@example.com",
      "accountEnabled": true,
      "userId": 42
    }
  ],
  "skipped": [],
  "groups": [
    {
      "groupId": 8,
      "slug": "finance",
      "name": "Finance",
      "added": 2
    }
  ],
  "seats": {
    "maxUsers": 50,
    "activeUsers": 12,
    "seatsRemaining": 38,
    "enforced": true
  },
  "unresolved": []
}

Import directory groups

POST /api/admin/directory/entra/import/groups

Links directory groups to permission groups and, optionally, brings in their members.

Links each directory group to a permission group, creating the permission group if none is linked yet. The IDs are processed in order:

  • The group is read from the directory. A new permission group takes the directory group's name and description, a slug derived from the name (or from the group's ID when the name has no usable characters) with a numeric suffix if it is taken, and source set to entra. An already-linked permission group takes the directory group's current name and description, and its source becomes entra; its slug and active state are kept.
  • With include_members, the group's direct user members are read. With provision_missing_users, members without an account get one, as in Import directory users. The group's entra membership is then brought in line with the directory: members who have an account are added, and entra members who are no longer in the directory group are removed. Members added by an administrator are kept.

Each group is saved as it is processed, so if the request fails part-way, for example because an ID can't be read from the directory or no licence seats remain, the work already done stays in place. Importing the same group again updates it rather than creating a second one.

Each entry of the response's groups reports entraGroupId, displayName, whether the permission group was created, its slug and groupId, the directoryMemberCount, usersCreated (the members given an account or, in a dry run or without provision_missing_users, who would be), usersSkipped (directory users with a reason), and membership, the numbers of entra members added and removed and of members skipped because no account matched. The member fields appear only with include_members. With dry_run, nothing is written: entries have no groupId or membership, and slug is the proposed slug before any numeric suffix.

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

Body parameters application/json

  • group_ids array of strings Required

    Object IDs of the directory groups to import, as returned by List directory groups. Also accepted as groupIds.

  • include_members boolean

    Read each group's direct user members and bring the permission group's entra membership in line with them. Also accepted as includeMembers.

    Defaults to true.

  • provision_missing_users boolean

    Create accounts for members who don't have one. Accounts disabled in the directory, and users without an email address there, are skipped. Has no effect unless include_members is true. Also accepted as provisionMissingUsers.

    Defaults to false.

  • default_role string

    The role for accounts created by provision_missing_users, as for Import directory users. Also accepted as defaultRole.

  • dry_run boolean

    Report what would happen without creating or changing groups, accounts or memberships. Also accepted as dryRun.

    Defaults to false.

Returns

Returns one entry for each directory group processed.

Errors

  • 400 group_ids is missing or empty, or the directory connection isn't configured.
  • 402 The licence doesn't currently allow adding users. Only requests that create accounts are affected.
  • 403 The licence limits active users and the members' new accounts don't fit in the free seats (the response's error is USER_LIMIT_REACHED); or the directory refused the request because the directory connection lacks a permission it needs.
  • 429 The directory is limiting the rate of requests. Try again shortly.
  • 502 The directory couldn't be reached or returned an error, for example because an ID in group_ids isn't a directory group.
Request
curl -X POST "$VDF_BASE_URL/api/admin/directory/entra/import/groups" \
  -H "Authorization: Bearer $VDF_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "group_ids": [
      "3f2b8c1e-5a4d-4e6f-9b7a-1c2d3e4f5a6b"
    ],
    "include_members": true,
    "provision_missing_users": true,
    "default_role": "default"
  }'
Response 200
{
  "success": true,
  "dryRun": false,
  "groups": [
    {
      "entraGroupId": "3f2b8c1e-5a4d-4e6f-9b7a-1c2d3e4f5a6b",
      "displayName": "Engineering",
      "created": true,
      "groupId": 7,
      "slug": "engineering",
      "directoryMemberCount": 2,
      "usersCreated": 1,
      "usersSkipped": [
        {
          "id": "c4f6a8b0-2d1e-4f3a-9b5c-7e0d2a4c6f81",
          "displayName": "Charles Babbage",
          "email": "charles@example.com",
          "userPrincipalName": "charles@example.com",
          "accountEnabled": false,
          "reason": "account is disabled in the directory"
        }
      ],
      "membership": {
        "added": 1,
        "removed": 0,
        "skipped": 0
      }
    }
  ]
}

Sync linked groups

POST /api/admin/directory/entra/sync

Reads from the directory the members of every directory group that a permission group is linked to (every permission group with an entraGroupId, active or not), and brings each permission group's entra membership in line: members who have an account are added, and entra members who are no longer in the directory group are removed. Members added by an administrator are kept, and the group's lastSyncedAt is updated.

A sync never creates, changes or deactivates accounts. Directory members without an account are counted in unprovisioned; give them accounts with Import directory users.

Each entry of the response's groups reports the groupId, slug, directoryMemberCount, unprovisioned and membership, as for Import directory groups. A group whose members can't be read is reported with an error, and the sync carries on with the others. With dry_run, nothing is changed and entries have no membership.

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

Body parameters application/json

  • dry_run boolean

    Report the directory's membership counts without changing any membership. Also accepted as dryRun.

    Defaults to false.

Returns

Returns one entry for each linked group.

Errors

  • 400 The directory connection isn't configured and at least one group is linked. remediable is true.
Request
curl -X POST "$VDF_BASE_URL/api/admin/directory/entra/sync" \
  -H "Authorization: Bearer $VDF_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "dry_run": false
  }'
Response 200
{
  "success": true,
  "dryRun": false,
  "groups": [
    {
      "groupId": 7,
      "slug": "engineering",
      "directoryMemberCount": 3,
      "unprovisioned": 1,
      "membership": {
        "added": 1,
        "removed": 0,
        "skipped": 0
      }
    }
  ]
}