---
title: Manage groups
description: Create groups, add and remove members, attach policies to a group,
  and read a group's membership history.
url: https://hexelstudio.com/docs/identity/groups
---

A group is a set of members of your organization. Policies attached to a group, and permission sets assigned to it, apply to every member of the group. Use groups to manage access for teams instead of attaching policies to people one by one.

Your organization already has some groups: the four membership groups behind the member roles, and a `default` group. See [Groups](https://hexelstudio.com/docs/identity/principals#groups) for what they are and which of them you can change.

## Permissions you need

| Step | Endpoint | Action |
| --- | --- | --- |
| Create, rename or delete a group | `POST /v1/groups`, `PATCH` and `DELETE /v1/groups/<group-id>` | `iam:group:write` |
| Add or remove members | `POST /v1/groups/<group-id>/members`, `DELETE /v1/groups/<group-id>/members/<user-id>` | `iam:group:manage_members` |
| Attach or detach policies | `POST /v1/policies/<policy-id>/attachments`, `DELETE .../attachments/<attachment-id>` | `iam:policy:write` |
| List groups, read a group, its members and its membership history | `GET /v1/groups`, `GET /v1/groups/<group-id>`, `.../members`, `.../audit-logs` | None: any member of the organization |

Owners hold every action. Admins hold these through IAM Admin Access. To add a member to a group you must also hold every action the group's policies allow (see [below](#add-members)), and to attach a policy, every action that policy allows.

## Create a group

<CodeGroup>

```bash curl
curl -X POST https://api.hexelstudio.com/v1/groups \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "platform-team",
    "display_name": "Platform team",
    "description": "Engineers who run our deployments"
  }'
```

```typescript TypeScript
const res = await fetch("https://api.hexelstudio.com/v1/groups", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.ACCESS_TOKEN}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    name: "platform-team",
    display_name: "Platform team",
    description: "Engineers who run our deployments",
  }),
});
if (!res.ok) {
  throw new Error(`Create group failed: ${res.status} ${await res.text()}`);
}
const group = await res.json();
console.log(group.id);
```

```python Python
import os
import requests

resp = requests.post(
    "https://api.hexelstudio.com/v1/groups",
    headers={"Authorization": f"Bearer {os.environ['ACCESS_TOKEN']}"},
    json={
        "name": "platform-team",
        "display_name": "Platform team",
        "description": "Engineers who run our deployments",
    },
    timeout=10,
)
resp.raise_for_status()
group = resp.json()
print(group["id"])
```

</CodeGroup>

<ParamField body="name" type="string">
  A short name, unique in your organization. `default` and the membership group names are taken.
</ParamField>
<ParamField body="display_name" type="string">
  The name shown in the console.
</ParamField>
<ParamField body="description" type="string">
  Optional description.
</ParamField>

The response is `201` with the group. The group belongs to the organization of your token.

```json
{
  "id": "grp_7d8e9f0a...",
  "organization_id": "org_9a8b7c...",
  "name": "platform-team",
  "display_name": "Platform team",
  "description": "Engineers who run our deployments",
  "group_type": "TEAM",
  "created_at": "2026-10-20T10:00:00.000000Z",
  "updated_at": "2026-10-20T10:00:00.000000Z"
}
```

A group name already used in your organization fails with `500` and the database error.

## Add members

`POST /v1/groups/<group-id>/members` adds one member, by user ID. Take the ID from the `user_id` field of the organization's members list.

```bash
curl -X POST https://api.hexelstudio.com/v1/groups/<group-id>/members \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"user_id": "<user-id>"}'
```

<ParamField body="user_id" type="string" required>
  The user ID of a member of your organization, or the ID of one of its service users (`su_...`).
</ParamField>

The response is `201`:

```json
{
  "id": "gm_3c4d5e6f...",
  "group_id": "grp_7d8e9f0a...",
  "user_id": "<user-id>",
  "added_by_user_id": "<your-user-id>",
  "added_at": "2026-10-20T10:05:00.000000Z"
}
```

The new member gets every policy attached to the group, and every permission set assigned to it, at once on their existing tokens. Because of that, you can add a member only if you hold every action those policies allow.

| Status | Body | Cause |
| --- | --- | --- |
| `400` | `{"error": "user_id is required"}` | `user_id` is missing. |
| `403` | `{"error": "forbidden"}` | You don't hold `iam:group:manage_members`. |
| `403` | `{"error": "forbidden", "reason": "cannot grant actions the caller does not hold", "missing_actions": [...]}` | The group grants actions you don't hold. |
| `403` | `{"error": "membership groups are managed through organization membership"}` | The group is a membership group. Change the member's role instead. |
| `404` | `{"error": "group not found"}` | No such group in your organization. |
| `422` | `{"error": "not_org_member", "message": "..."}` | `user_id` is not a member or service user of your organization. |
| `500` | `{"error": "..."}` | The user is already in the group. |
| `503` | `{"error": "membership lookup failed"}` | Membership couldn't be checked. Try again. |

<Note>
Removing someone from the organization also removes them from every group in it, including the groups you created, and removes the policies attached to them directly. If they join again, they get only the access of their new member role.
</Note>

### List members

```bash
curl https://api.hexelstudio.com/v1/groups/<group-id>/members \
  -H "Authorization: Bearer $ACCESS_TOKEN"
```

The response is an array of the membership objects shown above.

## Remove a member

```bash
curl -X DELETE https://api.hexelstudio.com/v1/groups/<group-id>/members/<user-id> \
  -H "Authorization: Bearer $ACCESS_TOKEN"
```

The response is `204`, also when the user was not in the group. The member loses the group's policies at once. Removing a member needs only `iam:group:manage_members`: you don't need to hold the group's policies.

## Attach policies to a group

Attach a policy with target type `GROUP`:

```bash
curl -X POST https://api.hexelstudio.com/v1/policies/<policy-id>/attachments \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"target_type": "GROUP", "target_id": "<group-id>"}'
```

The policy applies organization-wide to every member of the group. To grant a group access in one workspace or environment only, assign it a permission set with a scope and `"principal_type": "GROUP"`. Both are described in [Write and attach policies](https://hexelstudio.com/docs/identity/write-policies#attach-a-policy), with detaching and the errors.

You can attach policies to a membership group too, to give every member of that role more access. See [What a member gets by default](https://hexelstudio.com/docs/identity/principals#what-a-member-gets-by-default).

## Read and change a group

- `GET /v1/groups` lists every group of your organization, by name, including the membership groups and `default`.
- `GET /v1/groups/<group-id>` returns one group. A group of another organization returns `404` with `{"error": "group not found"}`.
- `PATCH /v1/groups/<group-id>` changes `name`, `display_name` or `description`. The response is `200` with the group.

```bash
curl -X PATCH https://api.hexelstudio.com/v1/groups/<group-id> \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"display_name": "Platform engineering"}'
```

Membership groups can't be changed or deleted: those calls return `403` with `{"error": "membership groups are managed through organization membership"}`.

Groups don't nest. A group has a `parent_group_id` field, but members of a group don't get the policies of its parent.

## Delete a group

```bash
curl -X DELETE https://api.hexelstudio.com/v1/groups/<group-id> \
  -H "Authorization: Bearer $ACCESS_TOKEN"
```

The response is `204`. Hexel Studio removes the group's members and the policies attached to it, so its members lose that access at once. Permission set assignments made to the group are not removed: delete them with `DELETE /v1/permission-sets/<permission-set-id>/assignments/<assignment-id>`. They grant nothing once the group is gone.

## Group audit logs

`GET /v1/groups/<group-id>/audit-logs` returns the group's membership history, newest first.

```bash
curl https://api.hexelstudio.com/v1/groups/<group-id>/audit-logs \
  -H "Authorization: Bearer $ACCESS_TOKEN"
```

```json
[
  {
    "id": "gal_8f9a0b1c...",
    "group_id": "grp_7d8e9f0a...",
    "action": "MEMBER_REMOVED",
    "target_user_id": "<user-id>",
    "created_at": "2026-10-21T08:00:00.000000Z"
  },
  {
    "id": "gal_2a3b4c5d...",
    "group_id": "grp_7d8e9f0a...",
    "action": "MEMBER_ADDED",
    "actor_user_id": "<your-user-id>",
    "target_user_id": "<user-id>",
    "created_at": "2026-10-20T10:05:00.000000Z"
  }
]
```

<ResponseField name="action" type="string">
  `MEMBER_ADDED` or `MEMBER_REMOVED`.
</ResponseField>
<ResponseField name="actor_user_id" type="string">
  Who added the member. Not recorded for removals.
</ResponseField>
<ResponseField name="target_user_id" type="string">
  The member added or removed.
</ResponseField>

This log holds membership changes made with the groups API only. Creating, renaming and deleting groups, attaching policies to them, and changes of member roles are recorded in the [audit logs](https://hexelstudio.com/docs/identity/audit-logs).
