Groups
A permission group is a named set of users. Owners of skills, tools, MCP servers, agents and workspaces can share them with a whole group at once, where their role allows it; see Share a resource. A group can be built by hand, or linked to a Microsoft Entra ID group so that its membership follows the directory; see Directory synchronisation.
Every membership records its source. Members added by an administrator are local; members added by a directory group import or sync are entra. A sync only adds and removes entra members, so the people you add by hand stay in the group whatever the directory says.
A member's access follows the groups they belonged to when they signed in, so membership changes take effect from their next sign-in. Only active groups give their members access.
Administrator access. Every endpoint on this page requires an administrator account.
- GET /admin/groups List groups
- POST /admin/groups Create a group
- GET /admin/groups/{group_id} Retrieve a group
- PUT /admin/groups/{group_id} Update a group
- DEL /admin/groups/{group_id} Delete a group
- GET /admin/groups/{group_id}/members List group members
- POST /admin/groups/{group_id}/members Add group members
- DEL /admin/groups/{group_id}/members/{user_id} Remove a group member
Paths are relative to /api
The group object
Retrieve a group also returns the group's members.
Attributes
-
idintegerUnique identifier for the group.
-
slugstringA unique identifier of lowercase letters, digits and hyphens, at most 64 characters. A row of Create users in bulk can name the group by its slug.
-
namestringThe group's name, at most 150 characters.
-
descriptionnullable stringWhat the group is for.
-
sourcestringlocalfor a group created by hand, orentrafor one that comes from Microsoft Entra ID. -
entraGroupIdnullable stringObject ID of the Microsoft Entra ID group this group is linked to, or
null. A directory sync keeps the membership of a linked group in step with the directory. -
entraTenantIdnullable stringA directory tenant identifier stored with the link, if one was supplied. Import and sync don't use it, and importing the directory group again clears it.
-
isActivebooleanWhether the group is active. Only active groups give their members access.
-
lastSyncedAtnullable stringWhen the group's directory-sourced membership was last brought in line with the directory, or
nullif it never has been. -
memberCountintegerThe number of members.
-
createdAtstringWhen the group was created, in ISO 8601 format.
-
updatedAtstringWhen the group was last changed or synchronised, in ISO 8601 format.
-
membersarray of objectsThe members, ordered by name. Returned by Retrieve a group only.
Show child attributes Hide child attributes
-
idintegerThe user's ID.
-
emailstringThe user's email address.
-
namestringThe user's display name.
-
userTypestringThe user's role.
-
isActivebooleanWhether the user's account is active.
-
sourcestringlocalfor a member added by an administrator, by hand or through an import's or bulk creation's group selection;entrafor a member added by a directory group import or sync. -
addedAtstringWhen the user joined the group, in ISO 8601 format.
-
{
"id": 7,
"slug": "engineering",
"name": "Engineering",
"description": "Engineering staff",
"source": "entra",
"entraGroupId": "3f2b8c1e-5a4d-4e6f-9b7a-1c2d3e4f5a6b",
"entraTenantId": null,
"isActive": true,
"lastSyncedAt": "2026-09-03T06:00:12.338190+00:00",
"memberCount": 2,
"createdAt": "2026-09-02T08:15:26.901233+00:00",
"updatedAt": "2026-09-03T06:00:12.338190+00:00"
} List groups
GET /api/admin/groups
Returns every permission group with its member count.
Returns all permission groups, active or not, ordered by name, each with its member count.
- Authentication
- Bearer token How it works
- Permission
- Requires an administrator account.
Parameters
No parameters.
Returns
Returns a list of group objects.
curl "$VDF_BASE_URL/api/admin/groups" \
-H "Authorization: Bearer $VDF_ACCESS_TOKEN" const response = await fetch(`${process.env.VDF_BASE_URL}/api/admin/groups`, {
headers: {
Authorization: `Bearer ${process.env.VDF_ACCESS_TOKEN}`,
},
});
if (!response.ok) throw new Error(`Request failed with status ${response.status}`);
const data = await response.json(); import os
import requests
response = requests.get(
f"{os.environ['VDF_BASE_URL']}/api/admin/groups",
headers={"Authorization": f"Bearer {os.environ['VDF_ACCESS_TOKEN']}"},
timeout=30,
)
response.raise_for_status()
data = response.json() {
"success": true,
"groups": [
{
"id": 7,
"slug": "engineering",
"name": "Engineering",
"description": "Engineering staff",
"source": "entra",
"entraGroupId": "3f2b8c1e-5a4d-4e6f-9b7a-1c2d3e4f5a6b",
"entraTenantId": null,
"isActive": true,
"lastSyncedAt": "2026-09-03T06:00:12.338190+00:00",
"memberCount": 2,
"createdAt": "2026-09-02T08:15:26.901233+00:00",
"updatedAt": "2026-09-03T06:00:12.338190+00:00"
},
{
"id": 8,
"slug": "finance",
"name": "Finance",
"description": "Finance team",
"source": "local",
"entraGroupId": null,
"entraTenantId": null,
"isActive": true,
"lastSyncedAt": null,
"memberCount": 0,
"createdAt": "2026-09-04T11:20:03.772915+00:00",
"updatedAt": "2026-09-04T11:20:03.772915+00:00"
}
]
} Create a group
POST /api/admin/groups
Creates a permission group.
Creates an active group with no members. To link it to a Microsoft Entra ID group, pass entra_group_id; the next directory sync then brings its membership in line with the directory. To create linked groups from the directory instead, use Import directory groups.
- Authentication
- Bearer token How it works
- Permission
- Requires an administrator account.
Body parameters application/json
-
namestring RequiredThe group's name, at most 150 characters.
-
slugstringA unique identifier of lowercase letters, digits and hyphens, 1 to 64 characters, starting and ending with a letter or digit. Upper-case letters are lowercased. Derived from
namewhen omitted. -
descriptionstringWhat the group is for.
-
sourcestringWhere the group comes from.
entrarequiresentra_group_id.Possible values-
local -
entra
-
-
entra_group_idstringObject ID (a GUID) of the Microsoft Entra ID group to link. A directory group can be linked to only one permission group. Also accepted as
entraGroupId. -
entra_tenant_idstringA directory tenant identifier to store with the link. Also accepted as
entraTenantId.
Returns
Returns the new group object.
Errors
- 400
nameis missing or longer than 150 characters; the slug (given or derived fromname) isn't valid;sourceisn'tlocalorentra;entra_group_idisn't a GUID; orsourceisentrawithoutentra_group_id. - 409 A group with this slug already exists, or the directory group is already linked to another group.
curl -X POST "$VDF_BASE_URL/api/admin/groups" \
-H "Authorization: Bearer $VDF_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Finance",
"description": "Finance team"
}' const response = await fetch(`${process.env.VDF_BASE_URL}/api/admin/groups`, {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.VDF_ACCESS_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: 'Finance',
description: 'Finance team',
}),
});
if (!response.ok) throw new Error(`Request failed with status ${response.status}`);
const data = await response.json(); import os
import requests
response = requests.post(
f"{os.environ['VDF_BASE_URL']}/api/admin/groups",
headers={"Authorization": f"Bearer {os.environ['VDF_ACCESS_TOKEN']}"},
json={
"name": "Finance",
"description": "Finance team",
},
timeout=30,
)
response.raise_for_status()
data = response.json() {
"success": true,
"group": {
"id": 8,
"slug": "finance",
"name": "Finance",
"description": "Finance team",
"source": "local",
"entraGroupId": null,
"entraTenantId": null,
"isActive": true,
"lastSyncedAt": null,
"memberCount": 0,
"createdAt": "2026-09-04T11:20:03.772915+00:00",
"updatedAt": "2026-09-04T11:20:03.772915+00:00"
}
} Retrieve a group
GET /api/admin/groups/{group_id}
Returns a permission group with its members.
Returns the group, whether active or not, together with its members.
- Authentication
- Bearer token How it works
- Permission
- Requires an administrator account.
Path parameters
-
group_idinteger RequiredThe group's ID.
Returns
Returns the group object, including members.
Errors
- 404 No group has this ID.
curl "$VDF_BASE_URL/api/admin/groups/7" \
-H "Authorization: Bearer $VDF_ACCESS_TOKEN" const response = await fetch(`${process.env.VDF_BASE_URL}/api/admin/groups/7`, {
headers: {
Authorization: `Bearer ${process.env.VDF_ACCESS_TOKEN}`,
},
});
if (!response.ok) throw new Error(`Request failed with status ${response.status}`);
const data = await response.json(); import os
import requests
response = requests.get(
f"{os.environ['VDF_BASE_URL']}/api/admin/groups/7",
headers={"Authorization": f"Bearer {os.environ['VDF_ACCESS_TOKEN']}"},
timeout=30,
)
response.raise_for_status()
data = response.json() {
"success": true,
"group": {
"id": 7,
"slug": "engineering",
"name": "Engineering",
"description": "Engineering staff",
"source": "entra",
"entraGroupId": "3f2b8c1e-5a4d-4e6f-9b7a-1c2d3e4f5a6b",
"entraTenantId": null,
"isActive": true,
"lastSyncedAt": "2026-09-03T06:00:12.338190+00:00",
"memberCount": 2,
"createdAt": "2026-09-02T08:15:26.901233+00:00",
"updatedAt": "2026-09-03T06:00:12.338190+00:00",
"members": [
{
"id": 42,
"email": "ada@example.com",
"name": "Ada Lovelace",
"userType": "default",
"isActive": true,
"source": "local",
"addedAt": "2026-09-02T09:02:11.574402+00:00"
},
{
"id": 43,
"email": "grace@example.com",
"name": "Grace Hopper",
"userType": "expert",
"isActive": true,
"source": "entra",
"addedAt": "2026-09-02T08:15:27.340118+00:00"
}
]
}
} Update a group
PUT /api/admin/groups/{group_id}
Replaces a permission group's settings.
Replaces the group's settings with the ones you send. Fields you omit are reset rather than kept: slug is derived from name again, description, entra_group_id and entra_tenant_id are cleared, source becomes local and is_active becomes true. To change one setting, send the group's current values with that one changed. Membership isn't affected.
Clearing entra_group_id unlinks the group from the directory, and its current members stay. Setting is_active to false keeps the group and its members, but the group stops giving access from its members' next sign-in.
- Authentication
- Bearer token How it works
- Permission
- Requires an administrator account.
Path parameters
-
group_idinteger RequiredThe group's ID.
Body parameters application/json
-
namestring RequiredThe group's name, at most 150 characters.
-
slugstringA unique identifier of lowercase letters, digits and hyphens, 1 to 64 characters, starting and ending with a letter or digit. Upper-case letters are lowercased. Derived from
namewhen omitted. -
descriptionstringWhat the group is for. Cleared when omitted.
-
sourcestringWhere the group comes from.
entrarequiresentra_group_id.Possible values-
local -
entra
-
-
entra_group_idstringObject ID (a GUID) of the linked Microsoft Entra ID group. A directory group can be linked to only one permission group. Omit it to unlink the group. Also accepted as
entraGroupId. -
entra_tenant_idstringA directory tenant identifier to store with the link. Also accepted as
entraTenantId. -
is_activebooleanWhether the group is active. Also accepted as
isActive.
Returns
Returns the updated group object.
Errors
- 400
nameis missing or longer than 150 characters; the slug (given or derived fromname) isn't valid;sourceisn'tlocalorentra;entra_group_idisn't a GUID; orsourceisentrawithoutentra_group_id. - 404 No group has this ID.
- 409 Another group already has this slug.
curl -X PUT "$VDF_BASE_URL/api/admin/groups/8" \
-H "Authorization: Bearer $VDF_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Finance and Legal",
"slug": "finance",
"description": "Finance and legal teams",
"is_active": true
}' const response = await fetch(`${process.env.VDF_BASE_URL}/api/admin/groups/8`, {
method: 'PUT',
headers: {
Authorization: `Bearer ${process.env.VDF_ACCESS_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: 'Finance and Legal',
slug: 'finance',
description: 'Finance and legal teams',
is_active: true,
}),
});
if (!response.ok) throw new Error(`Request failed with status ${response.status}`);
const data = await response.json(); import os
import requests
response = requests.put(
f"{os.environ['VDF_BASE_URL']}/api/admin/groups/8",
headers={"Authorization": f"Bearer {os.environ['VDF_ACCESS_TOKEN']}"},
json={
"name": "Finance and Legal",
"slug": "finance",
"description": "Finance and legal teams",
"is_active": True,
},
timeout=30,
)
response.raise_for_status()
data = response.json() {
"success": true,
"group": {
"id": 8,
"slug": "finance",
"name": "Finance and Legal",
"description": "Finance and legal teams",
"source": "local",
"entraGroupId": null,
"entraTenantId": null,
"isActive": true,
"lastSyncedAt": null,
"memberCount": 0,
"createdAt": "2026-09-04T11:20:03.772915+00:00",
"updatedAt": "2026-09-04T11:22:10.604419+00:00"
}
} Delete a group
DEL /api/admin/groups/{group_id}
Deletes a permission group and its memberships.
Permanently deletes the group and all of its memberships. The members' accounts aren't affected, and deleting a linked group doesn't change the directory. Members lose the access the group gave them from their next sign-in.
- Authentication
- Bearer token How it works
- Permission
- Requires an administrator account.
Path parameters
-
group_idinteger RequiredThe group's ID.
Returns
Returns a success marker.
Errors
- 404 No group has this ID.
curl -X DELETE "$VDF_BASE_URL/api/admin/groups/8" \
-H "Authorization: Bearer $VDF_ACCESS_TOKEN" const response = await fetch(`${process.env.VDF_BASE_URL}/api/admin/groups/8`, {
method: 'DELETE',
headers: {
Authorization: `Bearer ${process.env.VDF_ACCESS_TOKEN}`,
},
});
if (!response.ok) throw new Error(`Request failed with status ${response.status}`);
const data = await response.json(); import os
import requests
response = requests.delete(
f"{os.environ['VDF_BASE_URL']}/api/admin/groups/8",
headers={"Authorization": f"Bearer {os.environ['VDF_ACCESS_TOKEN']}"},
timeout=30,
)
response.raise_for_status()
data = response.json() {
"success": true
} List group members
GET /api/admin/groups/{group_id}/members
Returns the members of a permission group.
Returns the group's members, including deactivated accounts, ordered by name and then email address. Each member's source says whether a directory sync manages the membership.
- Authentication
- Bearer token How it works
- Permission
- Requires an administrator account.
Path parameters
-
group_idinteger RequiredThe group's ID.
Returns
Returns the list of members, each shaped like an entry of the group's members.
Errors
- 404 No group has this ID.
curl "$VDF_BASE_URL/api/admin/groups/7/members" \
-H "Authorization: Bearer $VDF_ACCESS_TOKEN" const response = await fetch(`${process.env.VDF_BASE_URL}/api/admin/groups/7/members`, {
headers: {
Authorization: `Bearer ${process.env.VDF_ACCESS_TOKEN}`,
},
});
if (!response.ok) throw new Error(`Request failed with status ${response.status}`);
const data = await response.json(); import os
import requests
response = requests.get(
f"{os.environ['VDF_BASE_URL']}/api/admin/groups/7/members",
headers={"Authorization": f"Bearer {os.environ['VDF_ACCESS_TOKEN']}"},
timeout=30,
)
response.raise_for_status()
data = response.json() {
"success": true,
"members": [
{
"id": 42,
"email": "ada@example.com",
"name": "Ada Lovelace",
"userType": "default",
"isActive": true,
"source": "local",
"addedAt": "2026-09-02T09:02:11.574402+00:00"
},
{
"id": 43,
"email": "grace@example.com",
"name": "Grace Hopper",
"userType": "expert",
"isActive": true,
"source": "entra",
"addedAt": "2026-09-02T08:15:27.340118+00:00"
}
]
} Add group members
POST /api/admin/groups/{group_id}/members
Adds the users to the group as local members, so a directory sync never removes them. Users who are already members are left as they are, including their source, and aren't counted in added. If any ID doesn't match an account, nothing is added.
- Authentication
- Bearer token How it works
- Permission
- Requires an administrator account.
Path parameters
-
group_idinteger RequiredThe group's ID.
Body parameters application/json
-
user_idsarray of integers RequiredIDs of the users to add. Also accepted as
userIds; a single ID can be sent asuser_idinstead.
Returns
Returns the number of users added and the group's members.
Errors
- 400
user_idsis missing, empty or not a list of integers, or an ID doesn't match any account. - 404 No group has this ID.
curl -X POST "$VDF_BASE_URL/api/admin/groups/8/members" \
-H "Authorization: Bearer $VDF_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"user_ids": [
42,
44
]
}' const response = await fetch(`${process.env.VDF_BASE_URL}/api/admin/groups/8/members`, {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.VDF_ACCESS_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
user_ids: [42, 44],
}),
});
if (!response.ok) throw new Error(`Request failed with status ${response.status}`);
const data = await response.json(); import os
import requests
response = requests.post(
f"{os.environ['VDF_BASE_URL']}/api/admin/groups/8/members",
headers={"Authorization": f"Bearer {os.environ['VDF_ACCESS_TOKEN']}"},
json={
"user_ids": [42, 44],
},
timeout=30,
)
response.raise_for_status()
data = response.json() {
"success": true,
"added": 2,
"members": [
{
"id": 42,
"email": "ada@example.com",
"name": "Ada Lovelace",
"userType": "default",
"isActive": true,
"source": "local",
"addedAt": "2026-09-04T11:25:40.118233+00:00"
},
{
"id": 44,
"email": "katherine@example.com",
"name": "Katherine Johnson",
"userType": "expert",
"isActive": true,
"source": "local",
"addedAt": "2026-09-04T11:25:40.118233+00:00"
}
]
} Remove a group member
DEL /api/admin/groups/{group_id}/members/{user_id}
Removes a user from a permission group.
Removes the user from the group, whatever the membership's source. The account isn't affected. If the membership came from the directory and the user is still in the linked directory group, the next sync adds them back.
- Authentication
- Bearer token How it works
- Permission
- Requires an administrator account.
Path parameters
-
group_idinteger RequiredThe group's ID.
-
user_idinteger RequiredThe member's user ID.
Returns
Returns a success marker.
Errors
- 404 The user isn't a member of this group, or no group has this ID.
curl -X DELETE "$VDF_BASE_URL/api/admin/groups/7/members/42" \
-H "Authorization: Bearer $VDF_ACCESS_TOKEN" const response = await fetch(`${process.env.VDF_BASE_URL}/api/admin/groups/7/members/42`, {
method: 'DELETE',
headers: {
Authorization: `Bearer ${process.env.VDF_ACCESS_TOKEN}`,
},
});
if (!response.ok) throw new Error(`Request failed with status ${response.status}`);
const data = await response.json(); import os
import requests
response = requests.delete(
f"{os.environ['VDF_BASE_URL']}/api/admin/groups/7/members/42",
headers={"Authorization": f"Bearer {os.environ['VDF_ACCESS_TOKEN']}"},
timeout=30,
)
response.raise_for_status()
data = response.json() {
"success": true
}