---
title: Members and invitations
description: Invite people to your organization, give them a member role, change
  it, and remove them.
url: https://hexelstudio.com/docs/identity/members
---

A member is a Hexel account that belongs to your organization. Every member has exactly one member role: owner, admin, member or viewer. The role decides the member's default access and which other members they can manage.

You add people in one of two ways:

- **Invite them by email.** This is the usual way. The person accepts the invitation and joins with the `org_member` role.
- **Add them directly.** Use this when the person already has a Hexel account and you know their user ID.

The examples on this page use `$ACCESS_TOKEN` (an access token for your organization) and `$ORG_ID`. See [Authenticate to the API](https://hexelstudio.com/docs/identity/authenticate).

## Member roles

| Role | Rank | Default access |
| --- | --- | --- |
| `org_owner` | 4 | Everything in the organization, including deleting it. |
| `org_admin` | 3 | Manage members and invitations, organization details and settings, policies, groups, roles and service users, workspaces and environments, and billing. Cannot delete the organization. |
| `org_member` | 2 | Read the organization, its settings and billing. Create and manage workspaces, environments and deployments. Cannot invite or manage members. |
| `org_viewer` | 1 | Read-only access, including policies, workspaces, environments and billing. |

Each role is backed by a group that Hexel Studio manages, such as **Organization admins**. The group's policies are the role's access. You can't add people to these groups or remove them yourself: change the member's role instead. To give a member more than their role allows, attach a policy to them or to one of your own [groups](https://hexelstudio.com/docs/identity/groups).

The person who creates an organization is its first owner.

### Rank rules

Ranks control who can manage whom:

- **Assigning a role.** The role you assign must rank below your own. An admin can assign `org_member` and `org_viewer`. An owner can also assign `org_admin`. Nobody can assign `org_owner` with a role change, because no role ranks above it. An owner makes someone an owner by [transferring ownership](#transfer-ownership).
- **Changing or removing a member.** You must outrank the member's current role. Two exceptions: an owner can change or remove any member, including another owner, and anyone can lower or remove their own membership.
- **The last owner.** An organization always keeps at least one owner. Removing or demoting the only owner fails with `409 last_owner`.

A request that breaks a rank rule fails with `403`:

```json
{ "error": "cannot assign role equal or higher than your own" }
```

```json
{ "error": "cannot change a member whose role is equal to or higher than your own" }
```

### Required permissions

| Operation | Permission |
| --- | --- |
| List members | `org:org:read` |
| Add a member, or create, list, resend and revoke invitations | `org:member:invite` |
| Change a member's role, or remove a member (including yourself) | `org:member:manage` |

By default, owners and admins hold all three. Members and viewers hold only `org:org:read`.

## List members

```bash
curl https://api.hexelstudio.com/v1/organizations/$ORG_ID/members \
  -H "Authorization: Bearer $ACCESS_TOKEN"
```

The response is an array of memberships:

```json
[
  {
    "id": "<member-id>",
    "organization_id": "<org-id>",
    "user_id": "<user-id>",
    "role": "org_admin",
    "status": "ACTIVE",
    "joined_at": "2026-10-01T09:30:00Z",
    "created_at": "2026-10-01T09:30:00Z",
    "updated_at": "2026-10-01T09:30:00Z"
  }
]
```

The `id` is the membership ID. Use it to change or remove the member. It is not the user ID.

To include each member's profile, call `/members/enriched`. Each entry has the same fields plus `email`, `display_name`, `first_name`, `last_name` and `profile_image_url` when the profile has them.

```bash
curl https://api.hexelstudio.com/v1/organizations/$ORG_ID/members/enriched \
  -H "Authorization: Bearer $ACCESS_TOKEN"
```

## Invite people

### Create an invitation

<CodeGroup>

```bash curl
curl -X POST https://api.hexelstudio.com/v1/organizations/$ORG_ID/invitations \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"email": "teammate@example.com"}'
```

```typescript TypeScript
const res = await fetch(
  `https://api.hexelstudio.com/v1/organizations/${process.env.ORG_ID}/invitations`,
  {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.ACCESS_TOKEN}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ email: "teammate@example.com" }),
  },
);
const invitation = await res.json();
```

```python Python
import os
import requests

res = requests.post(
    f"https://api.hexelstudio.com/v1/organizations/{os.environ['ORG_ID']}/invitations",
    headers={"Authorization": f"Bearer {os.environ['ACCESS_TOKEN']}"},
    json={"email": "teammate@example.com"},
)
invitation = res.json()
```

</CodeGroup>

<ParamField body="email" type="string" required>
  The address to send the invitation to.
</ParamField>

The response is `201 Created` with the invitation:

```json
{
  "id": "<invitation-id>",
  "organization_id": "<org-id>",
  "email": "teammate@example.com",
  "invited_by_user_id": "<your-user-id>",
  "role_name": "",
  "role_id": "",
  "invitation_token": "<invitation-token>",
  "status": "PENDING",
  "expires_at": "2026-10-27T09:30:00Z",
  "created_at": "2026-10-20T09:30:00Z"
}
```

Hexel Studio emails the address a link to the console. The person signs in with their Hexel account and accepts. An invitation expires 7 days after it is created or last resent.

### Accept an invitation

The console accepts the invitation when the person follows the link. To accept through the API, call this endpoint with an access token for the person's own account. The token does not have to be for the inviting organization.

```bash
curl -X POST https://api.hexelstudio.com/v1/invitations/<invitation-token>/accept \
  -H "Authorization: Bearer $ACCESS_TOKEN"
```

The response is the new membership. The person joins with the `org_member` role. An owner or admin can then [change the role](#change-a-members-role).

Accepting fails with `400` when the token is unknown, already used, revoked or expired, and with `403 quota_exceeded` when the organization is at its member limit.

### Look up an invitation

Before accepting, the person can see what the invitation is for. Call this endpoint with an access token for the person's own account:

```bash
curl https://api.hexelstudio.com/v1/invitations/<invitation-token> \
  -H "Authorization: Bearer $ACCESS_TOKEN"
```

```json
{
  "organization": { "id": "org_...", "name": "Acme" },
  "role": "org_member",
  "email": "dev@example.com",
  "status": "PENDING",
  "expires_at": "2026-10-31T12:00:00Z",
  "created_at": "2026-10-24T12:00:00Z",
  "invited_by": { "user_id": "...", "name": "Jane Doe" }
}
```

`status` is `PENDING`, `ACCEPTED`, `REJECTED`, `EXPIRED` or `REVOKED`; a pending invitation past its expiry reads `EXPIRED`. Like accepting, anyone signed in who has the link can look it up. The response never includes the token. An unknown token returns `404`.

### List invitations

```bash
curl https://api.hexelstudio.com/v1/organizations/$ORG_ID/invitations \
  -H "Authorization: Bearer $ACCESS_TOKEN"
```

The list includes invitations in every state. `status` is one of:

| Status | Meaning |
| --- | --- |
| `PENDING` | Sent and not yet accepted. |
| `ACCEPTED` | The person joined. `accepted_at` is set. |
| `REVOKED` | You revoked it. `revoked_at` is set. |
| `EXPIRED` | It passed `expires_at` without being accepted. Hexel Studio marks expired invitations once an hour; an invitation past `expires_at` can't be accepted even before it is marked. |

### Resend an invitation

Resending emails the link again and moves `expires_at` to 7 days from now. Only pending invitations can be resent.

```bash
curl -X POST https://api.hexelstudio.com/v1/organizations/$ORG_ID/invitations/<invitation-id>/resend \
  -H "Authorization: Bearer $ACCESS_TOKEN"
```

```json
{ "message": "invitation resent", "expires_at": "2026-10-27T09:30:00Z" }
```

### Revoke an invitation

```bash
curl -X POST https://api.hexelstudio.com/v1/organizations/$ORG_ID/invitations/<invitation-id>/revoke \
  -H "Authorization: Bearer $ACCESS_TOKEN"
```

```json
{ "message": "invitation revoked" }
```

Only pending invitations can be revoked. Anything else returns `404` with `"invitation not found or not pending"`.

## Add a member directly

If the person already has a Hexel account, you can add them with their user ID and a role. You need `org:member:invite`, and the role must rank below your own.

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

<ParamField body="user_id" type="string" required>
  The person's Hexel user ID. The account must exist.
</ParamField>

<ParamField body="role" type="string" required>
  One of `org_viewer`, `org_member` or `org_admin`. Any other value returns `400` with `"invalid role"`.
</ParamField>

The response is `201 Created` with the membership.

## Change a member's role

```bash
curl -X PATCH https://api.hexelstudio.com/v1/organizations/$ORG_ID/members/<member-id> \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"role": "org_viewer"}'
```

<ParamField body="role" type="string" required>
  The new role. It must rank below your own.
</ParamField>

The response is the updated membership. The member's access changes right away: Hexel Studio moves them to the group for the new role.

Demoting the organization's only owner fails:

```json
{
  "error": "last_owner",
  "message": "the organization must keep at least one owner"
}
```

## Transfer ownership

An owner makes another member an owner. By default you become an admin in the same step, so the organization always has an owner:

```bash
curl -X POST https://api.hexelstudio.com/v1/organizations/$ORG_ID/transfer-ownership \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"member_id": "<member-id>"}'
```

<ParamField body="member_id" type="string" required>
  The membership ID of the new owner, from [List members](#list-members). They must already be a member of the organization.
</ParamField>

<ParamField body="keep_ownership" type="boolean">
  Stay an owner yourself, so the organization has two owners. Defaults to `false`.
</ParamField>

The response has both memberships (shortened here):

```json
{
  "previous_owner": { "id": "mem_...", "user_id": "...", "role": "org_admin" },
  "new_owner": { "id": "mem_...", "user_id": "...", "role": "org_owner" }
}
```

Both members' access changes right away. Only an owner can transfer ownership; anyone else gets `403 not_owner`. Naming yourself fails with `400 transfer_to_self`, and a member of another organization returns `404`.

Transfer ownership before you leave an organization you own, or before you [delete your account](https://hexelstudio.com/docs/identity/your-data#delete-your-account).

## Remove a member

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

The response is `204 No Content`. To leave an organization, remove your own membership. This also needs `org:member:manage`. Removing the only owner fails with `409 last_owner`.

Hexel Studio has no separate suspend for members. To cut off a person's access, remove them.

### What happens to their access

When you remove a member, Hexel Studio:

- Deletes the membership and the person's workspace memberships in the organization.
- Removes all of their access in the organization: every group they belong to (their member role's group, the **Default Team** and your own groups), policies attached to them directly, permission-set assignments, permission boundaries, temporary credentials and open just-in-time requests.
- Keeps the service users they created, because those belong to the organization, and keeps the audit history.
- Stops issuing and refreshing access tokens for the organization to them. An access token they already hold stays valid until it expires. See [Tokens](https://hexelstudio.com/docs/identity/tokens).

If you invite them again later, they start with only the access of their new member role.

## Member limits

Two plan limits apply. See [Limits](https://hexelstudio.com/docs/identity/limits) for your plan's values.

| Limit | What counts |
| --- | --- |
| `members_per_org` | Members plus pending invitations. Creating an invitation or adding a member fails when the total has reached the limit. Accepting an invitation counts members only, because the invitation already held a place. |
| `invitations_per_day` | Invitations created or resent in the last 24 hours. |

A request over a limit fails with `403`:

```json
{
  "error": "quota_exceeded",
  "quota_key": "members_per_org",
  "limit": 5,
  "current": 5,
  "message": "plan limit reached: this organization can have at most 5 members, including pending invitations; upgrade the plan or contact Hexel"
}
```

Revoke pending invitations you no longer need to free their places.
