---
title: "Members, roles and tokens"
description: "Who can do what in a project: members, roles, environment rules, tokens, notification settings and the audit log."
order: 5
---

Who can do what in a project: members, roles, environment rules, tokens, notification settings and the audit log.

## `GET /api/v1/me`

Who the caller is: memberships with their permissions, the environment rules that apply, and the project a token is pinned to, if any.

| Status | Means | Body |
| --- | --- | --- |
| 200 | Done | [MeResponse](#meresponse) |
| 401 | No token, or a token that is not valid |  |

## `DELETE /api/v1/me/mfa`

Drop the caller's own enrolment.

| Status | Means | Body |
| --- | --- | --- |
| 204 | Done, with no body |  |
| 401 | No token, or a token that is not valid |  |
| 409 | It conflicts with what is there |  |

## `POST /api/v1/me/mfa/confirm`

Prove one code from the staged seed and the enrolment is in force. A wrong code and a replayed code are the same 401, deliberately.

**Body:** [MfaConfirmBody](#mfaconfirmbody)

| Status | Means | Body |
| --- | --- | --- |
| 204 | Done, with no body |  |
| 401 | No token, or a token that is not valid |  |
| 409 | Nothing is waiting to be confirmed |  |
| 503 | The secrets store is sealed |  |

## `POST /api/v1/me/mfa/enrol`

Start an enrolment: a fresh seed, staged but not yet in force. The account is only protected once a code from it verifies, so this call alone locks nobody out and can be repeated.

| Status | Means | Body |
| --- | --- | --- |
| 200 | Done | [MfaEnrolResponse](#mfaenrolresponse) |
| 401 | No token, or a token that is not valid |  |
| 409 | This account already has an authenticator enrolled |  |
| 503 | The secrets store is sealed |  |

## `GET /api/v1/me/sessions`

The caller's own live sessions: every browser and CLI they are signed in on.

| Status | Means | Body |
| --- | --- | --- |
| 200 | Done | array of [SessionResponse](#sessionresponse) |
| 401 | No token, or a token that is not valid |  |

## `DELETE /api/v1/me/sessions/{session_id}`

Sign one of the caller's own sessions out.

| Parameter | In | Type | About |
| --- | --- | --- | --- |
| `session_id` | path | string (uuid) |  |

| Status | Means | Body |
| --- | --- | --- |
| 204 | Done, with no body |  |
| 401 | No token, or a token that is not valid |  |
| 404 | Not found |  |

## `GET /api/v1/notifications`

The caller's feed, newest first.

| Parameter | In | Type | About |
| --- | --- | --- | --- |
| `unread` (optional) | query | boolean or null |  |
| `project` (optional) | query | string or null | A project slug, matched against the notice's own `project` param. |
| `limit` (optional) | query | integer or null | Default 50, maximum 200. |
| `cursor` (optional) | query | string or null |  |

| Status | Means | Body |
| --- | --- | --- |
| 200 | Done | array of [NotificationResponse](#notificationresponse) |
| 401 | No token, or a token that is not valid |  |

## `POST /api/v1/notifications/read-all`

Mark the whole feed read. Idempotent, and never an error on an empty feed.

| Status | Means | Body |
| --- | --- | --- |
| 204 | Done, with no body |  |
| 401 | No token, or a token that is not valid |  |

## `GET /api/v1/notifications/unread-count`

How many notifications the caller has not read.

| Status | Means | Body |
| --- | --- | --- |
| 200 | Done | [UnreadCountResponse](#unreadcountresponse) |
| 401 | No token, or a token that is not valid |  |

## `POST /api/v1/notifications/{id}/read`

Mark one notice read.

| Parameter | In | Type | About |
| --- | --- | --- | --- |
| `id` | path | string (uuid) |  |

| Status | Means | Body |
| --- | --- | --- |
| 204 | Done, with no body |  |
| 401 | No token, or a token that is not valid |  |
| 404 | Not found |  |

## `GET /api/v1/permissions`

The 28 permissions, in console order with the groups contiguous, so the Roles page renders the toggles without sorting anything.

| Status | Means | Body |
| --- | --- | --- |
| 200 | Done | array of [PermissionResponse](#permissionresponse) |
| 401 | No token, or a token that is not valid |  |

## `GET /api/v1/projects/{slug}/audit`

One project's audit log: the same trail with the project pinned, so a project page can show its own refusals.

| Parameter | In | Type | About |
| --- | --- | --- | --- |
| `slug` | path | string |  |
| `actor` (optional) | query | string or null |  |
| `project` (optional) | query | string or null | Ignored on the project route: the path pins the project there. |
| `environment` (optional) | query | string or null |  |
| `action` (optional) | query | string or null |  |
| `outcome` (optional) | query | string or null | allowed \| denied. Absent = both. |
| `since` (optional) | query | string or null |  |
| `limit` (optional) | query | integer or null | Default 100, maximum 500. |
| `cursor` (optional) | query | integer or null |  |

| Status | Means | Body |
| --- | --- | --- |
| 200 | Done | array of [AuditResponse](#auditresponse) |
| 403 | Your role does not allow this |  |
| 404 | No such project |  |

## `GET /api/v1/projects/{slug}/environment-policies`

Every policy in the project, for its Environment access panel.

| Parameter | In | Type | About |
| --- | --- | --- | --- |
| `slug` | path | string |  |

| Status | Means | Body |
| --- | --- | --- |
| 200 | Done | array of [EnvironmentPolicyResponse](#environmentpolicyresponse) |
| 401 | No token, or a token that is not valid |  |
| 403 | Your role does not allow this |  |

## `PUT /api/v1/projects/{slug}/environment-policies/{environment}/{role_id}`

Write a policy.

| Parameter | In | Type | About |
| --- | --- | --- | --- |
| `slug` | path | string |  |
| `environment` | path | string |  |
| `role_id` | path | string (uuid) |  |

**Body:** [WritePolicyBody](#writepolicybody)

| Status | Means | Body |
| --- | --- | --- |
| 200 | Done | [EnvironmentPolicyResponse](#environmentpolicyresponse) |
| 401 | No token, or a token that is not valid |  |
| 403 | Your role does not allow this |  |
| 404 | Not found |  |
| 409 | The fixed role |  |
| 422 | Refused, and the error says why |  |

## `DELETE /api/v1/projects/{slug}/environment-policies/{environment}/{role_id}`

Remove a policy, which gives the role its full set back in that environment.

| Parameter | In | Type | About |
| --- | --- | --- | --- |
| `slug` | path | string |  |
| `environment` | path | string |  |
| `role_id` | path | string (uuid) |  |

| Status | Means | Body |
| --- | --- | --- |
| 204 | Done, with no body |  |
| 401 | No token, or a token that is not valid |  |
| 403 | Your role does not allow this |  |
| 404 | Not found |  |

## `GET /api/v1/projects/{slug}/members`

The project's members with their roles.

| Parameter | In | Type | About |
| --- | --- | --- | --- |
| `slug` | path | string |  |

| Status | Means | Body |
| --- | --- | --- |
| 200 | Done | array of [MemberResponse](#memberresponse) |
| 401 | No token, or a token that is not valid |  |
| 403 | Your role does not allow this |  |

## `POST /api/v1/projects/{slug}/members`

Add a member. One membership per operator per project, so adding someone who is already a member changes their role rather than failing.

| Parameter | In | Type | About |
| --- | --- | --- | --- |
| `slug` | path | string |  |

**Body:** [WriteMembershipBody](#writemembershipbody)

| Status | Means | Body |
| --- | --- | --- |
| 204 | Done, with no body |  |
| 401 | No token, or a token that is not valid |  |
| 403 | Your role does not allow this |  |
| 404 | Not found |  |
| 409 | It conflicts with what is there |  |

## `GET /api/v1/projects/{slug}/members/candidates`

The people who can be added as members of the project.

| Parameter | In | Type | About |
| --- | --- | --- | --- |
| `slug` | path | string |  |

| Status | Means | Body |
| --- | --- | --- |
| 200 | Done | array of [CandidateResponse](#candidateresponse) |
| 401 | No token, or a token that is not valid |  |
| 403 | Your role does not allow this |  |

## `PUT /api/v1/projects/{slug}/members/{operator_id}`

Change one member's role. The path names the operator; a mismatched `operator_id` in the body is ignored, because the path is what was authorized.

| Parameter | In | Type | About |
| --- | --- | --- | --- |
| `slug` | path | string |  |
| `operator_id` | path | string (uuid) |  |

**Body:** [WriteMembershipBody](#writemembershipbody)

| Status | Means | Body |
| --- | --- | --- |
| 204 | Done, with no body |  |
| 401 | No token, or a token that is not valid |  |
| 403 | Your role does not allow this |  |
| 404 | Not found |  |
| 409 | It conflicts with what is there |  |

## `DELETE /api/v1/projects/{slug}/members/{operator_id}`

Take a project membership away.

| Parameter | In | Type | About |
| --- | --- | --- | --- |
| `slug` | path | string |  |
| `operator_id` | path | string (uuid) |  |

| Status | Means | Body |
| --- | --- | --- |
| 204 | Done, with no body |  |
| 401 | No token, or a token that is not valid |  |
| 403 | Your role does not allow this |  |
| 404 | Not found |  |

## `GET /api/v1/projects/{slug}/notification-settings`

The caller's notification settings for this project, one for each kind of notice.

| Parameter | In | Type | About |
| --- | --- | --- | --- |
| `slug` | path | string |  |

| Status | Means | Body |
| --- | --- | --- |
| 200 | Done | array of [NotificationSettingResponse](#notificationsettingresponse) |
| 403 | Your role does not allow this |  |
| 404 | No such project |  |

## `PUT /api/v1/projects/{slug}/notification-settings`

Replace the caller's notification settings for this project.

| Parameter | In | Type | About |
| --- | --- | --- | --- |
| `slug` | path | string |  |

**Body:** [WriteNotificationSettingsBody](#writenotificationsettingsbody)

| Status | Means | Body |
| --- | --- | --- |
| 204 | Done, with no body |  |
| 403 | Your role does not allow this |  |
| 404 | No such project |  |
| 422 | Unknown notification kind |  |

## `GET /api/v1/projects/{slug}/roles`

The roles every project has, plus the ones this project defined.

| Parameter | In | Type | About |
| --- | --- | --- | --- |
| `slug` | path | string |  |

| Status | Means | Body |
| --- | --- | --- |
| 200 | Done | array of [RoleResponse](#roleresponse) |
| 401 | No token, or a token that is not valid |  |
| 403 | Your role does not allow this |  |

## `POST /api/v1/projects/{slug}/roles`

Create a role this project owns. The path decides the project; a `project` in the body is ignored, because the permission that let this call through is `project.members` in THIS project.

| Parameter | In | Type | About |
| --- | --- | --- | --- |
| `slug` | path | string |  |

**Body:** [WriteRoleBody](#writerolebody)

| Status | Means | Body |
| --- | --- | --- |
| 201 | Created | [RoleResponse](#roleresponse) |
| 401 | No token, or a token that is not valid |  |
| 403 | Your role does not allow this |  |
| 404 | Not found |  |
| 409 | It conflicts with what is there |  |
| 422 | Refused, and the error says why |  |

## `PUT /api/v1/projects/{slug}/roles/{id}`

Change a role this project defined: its name and its permissions.

| Parameter | In | Type | About |
| --- | --- | --- | --- |
| `slug` | path | string |  |
| `id` | path | string (uuid) |  |

**Body:** [WriteRoleBody](#writerolebody)

| Status | Means | Body |
| --- | --- | --- |
| 200 | Done | [RoleResponse](#roleresponse) |
| 401 | No token, or a token that is not valid |  |
| 403 | Your role does not allow this |  |
| 404 | Not found |  |
| 409 | It conflicts with what is there |  |
| 422 | Refused, and the error says why |  |

## `DELETE /api/v1/projects/{slug}/roles/{id}`

Remove a role this project defined.

| Parameter | In | Type | About |
| --- | --- | --- | --- |
| `slug` | path | string |  |
| `id` | path | string (uuid) |  |

| Status | Means | Body |
| --- | --- | --- |
| 204 | Done, with no body |  |
| 401 | No token, or a token that is not valid |  |
| 403 | Your role does not allow this |  |
| 404 | Not found |  |
| 409 | That role still has members |  |

## `GET /api/v1/projects/{slug}/tokens`

Every token pinned to this project, for its settings page.

| Parameter | In | Type | About |
| --- | --- | --- | --- |
| `slug` | path | string |  |

| Status | Means | Body |
| --- | --- | --- |
| 200 | Done | array of [TokenResponse](#tokenresponse) |
| 401 | No token, or a token that is not valid |  |
| 403 | Your role does not allow this |  |

## `GET /api/v1/tokens`

The caller's own tokens, and every token when the caller manages people.

| Status | Means | Body |
| --- | --- | --- |
| 200 | Done | array of [TokenResponse](#tokenresponse) |
| 401 | No token, or a token that is not valid |  |

## `POST /api/v1/tokens`

Mint a token for the caller. The secret is in the answer and nowhere else.

**Body:** [MintTokenBody](#minttokenbody)

| Status | Means | Body |
| --- | --- | --- |
| 201 | Created | [TokenMintedResponse](#tokenmintedresponse) |
| 401 | No token, or a token that is not valid |  |
| 403 | A machine operator cannot mint tokens |  |
| 404 | Not found |  |
| 422 | A token cannot carry more than the caller does |  |

## `DELETE /api/v1/tokens/{id}`

Revoke a token, instantly. An operator revokes their own; one who manages operators revokes anyone's.

| Parameter | In | Type | About |
| --- | --- | --- | --- |
| `id` | path | string (uuid) |  |

| Status | Means | Body |
| --- | --- | --- |
| 204 | Done, with no body |  |
| 401 | No token, or a token that is not valid |  |
| 404 | Not found |  |

## Types

### MeResponse

| Field | Type | About |
| --- | --- | --- |
| `actor` | string | The audit actor string: the operator's email, or `api-token`. |
| `display_name` | string |  |
| `kind` | string | person \| machine |
| `machine` | boolean |  |
| `memberships` | array of [MeMembership](#memembership) |  |
| `mfa` | boolean | Whether this operator has a TOTP authenticator enrolled. |
| `operator_id` | string (uuid) |  |
| `policies` | array of [MePolicy](#mepolicy) |  |
| `suspended` | boolean |  |
| `token_id` (optional) | string (uuid) or null |  |
| `token_project` (optional) | string or null | A value means the token acts in this project and nowhere else. |
| `token_scopes` (optional) | array of string or null | null = a session bearer, which is never narrowed. A value means a minted token's snapshot of its role at mint time. |
| `via` | string | console \| token \| api-token |

### MfaConfirmBody

| Field | Type | About |
| --- | --- | --- |
| `code` | string |  |

### MfaEnrolResponse

| Field | Type | About |
| --- | --- | --- |
| `secret` | string | Base32, for manual entry. Shown once and never again. |
| `uri` | string | The otpauth:// URI the authenticator app scans. |

### SessionResponse

| Field | Type | About |
| --- | --- | --- |
| `created_at` | string |  |
| `current` | boolean | The session this very request arrived on. |
| `expires_at` | string |  |
| `id` | string (uuid) |  |
| `ip` | string |  |
| `kind` | string | console \| cli |
| `user_agent` | string |  |

### NotificationResponse

| Field | Type | About |
| --- | --- | --- |
| `created_at` | string |  |
| `id` | string (uuid) |  |
| `kind` | string | build.failed \| deployment.rolled \| deployment.refused \| deployment.auto_rollback \| probe.down \| quota.high \| package.published |
| `link` (optional) | string or null | The console path the notice opens, relative. |
| `params` | any | PII free: slugs, names of things, numbers. |
| `read` | boolean |  |

### UnreadCountResponse

The badge in the operator menu: global, never per project.

| Field | Type | About |
| --- | --- | --- |
| `unread` | integer |  |

### PermissionResponse

| Field | Type | About |
| --- | --- | --- |
| `group` | string |  |
| `hint` | string |  |
| `key` | string |  |
| `label` | string |  |

### AuditResponse

| Field | Type | About |
| --- | --- | --- |
| `action` | string | The permission that was asked for, or the verb of a self service write. |
| `actor` | string | The operator email, or `api-token` / `github-app` for a machine caller. |
| `at` | string |  |
| `environment` (optional) | string or null |  |
| `id` | integer |  |
| `ip` | string |  |
| `outcome` | string | allowed \| denied |
| `project` (optional) | string or null |  |
| `reason` | string | The deny reason of a refusal, empty when allowed. |
| `target` | string |  |
| `via` | string | console \| token \| api-token \| system |

### EnvironmentPolicyResponse

| Field | Type | About |
| --- | --- | --- |
| `allowed` | array of string |  |
| `environment` | string |  |
| `removed` | array of string | What this policy takes away from the role, for the console's muted line. |
| `role_id` | string (uuid) |  |
| `role_name` | string |  |
| `updated_at` | string |  |

### WritePolicyBody

| Field | Type | About |
| --- | --- | --- |
| `allowed` | array of string |  |

### MemberResponse

| Field | Type | About |
| --- | --- | --- |
| `display_name` | string |  |
| `email` | string |  |
| `kind` | string | person \| machine |
| `operator_id` | string (uuid) |  |
| `project` (optional) | string or null |  |
| `project_name` (optional) | string or null |  |
| `role_id` | string (uuid) |  |
| `role_name` | string |  |
| `role_slug` | string |  |
| `since` | string |  |

### WriteMembershipBody

| Field | Type | About |
| --- | --- | --- |
| `operator_id` | string (uuid) |  |
| `role_id` | string (uuid) |  |

### CandidateResponse

One operator the Add member form may offer: identity only, no memberships, no sessions, no MFA state, `kind` person | machine.

| Field | Type | About |
| --- | --- | --- |
| `display_name` | string |  |
| `email` | string |  |
| `id` | string (uuid) |  |
| `kind` | string |  |

### NotificationSettingResponse

One kind's two channels, with the label the section draws.

| Field | Type | About |
| --- | --- | --- |
| `email` | boolean |  |
| `in_app` | boolean |  |
| `kind` | string |  |
| `label` (optional) | string | Ignored on the way in: the label belongs to the API, not to the form. |

### WriteNotificationSettingsBody

| Field | Type | About |
| --- | --- | --- |
| `settings` | array of [NotificationSettingResponse](#notificationsettingresponse) |  |

### RoleResponse

| Field | Type | About |
| --- | --- | --- |
| `description` | string |  |
| `display_name` | string |  |
| `fixed` | boolean |  |
| `id` | string (uuid) |  |
| `members` | integer |  |
| `permissions` | array of string | The console keys on `fixed`, not on this. |
| `project` (optional) | string or null |  |
| `slug` | string |  |

### WriteRoleBody

| Field | Type | About |
| --- | --- | --- |
| `description` (optional) | string |  |
| `display_name` | string |  |
| `permissions` | array of string |  |
| `project` (optional) | string or null | Ignored on PUT and on the project scoped routes, where the path decides: a role never moves. |
| `slug` | string |  |

### TokenResponse

| Field | Type | About |
| --- | --- | --- |
| `created_at` | string |  |
| `expires_at` (optional) | string or null |  |
| `id` | string (uuid) |  |
| `kind` | string | api \| cli \| mcp |
| `last_used` (optional) | string or null |  |
| `name` | string |  |
| `operator` | string | The operator's email, or the machine's handle. |
| `operator_id` | string (uuid) |  |
| `project` (optional) | string or null | null = the token acts wherever its operator may. |
| `revoked` | boolean |  |
| `role_id` | string (uuid) |  |
| `role_name` | string |  |
| `scopes` | array of string |  |

### MintTokenBody

| Field | Type | About |
| --- | --- | --- |
| `expires_in_days` (optional) | integer or null | Days until expiry. Omitted = `OPS_TOKEN_TTL_DAYS`, 0 = no expiry. |
| `kind` (optional) | string or null | api (default) \| cli \| mcp |
| `name` | string |  |
| `project` (optional) | string or null | null = the token acts wherever its operator may; a slug pins it. |
| `role_id` | string (uuid) |  |

### TokenMintedResponse

| Field | Type | About |
| --- | --- | --- |
| `secret` | string | The raw bearer, `opst_…`. Shown ONCE: it is never stored and cannot be read back, so a lost secret is a new token. |
| `token` | [TokenResponse](#tokenresponse) |  |

### MeMembership

| Field | Type | About |
| --- | --- | --- |
| `fixed` | boolean |  |
| `permissions` | array of string |  |
| `project` (optional) | string or null |  |
| `role_id` | string (uuid) |  |
| `role_name` | string |  |
| `role_slug` | string |  |

### MePolicy

| Field | Type | About |
| --- | --- | --- |
| `allowed` | array of string |  |
| `environment` | string |  |
| `project` | string |  |
| `role_id` | string (uuid) |  |
