GET/api/v1/directory-groups
GET /api/directory-groups — every group in the org (manager+).
Responses
| Status | Description |
|---|
200 | |
403 | requires manage_users, manage_roles, directory_groups:read or directory_groups:write |
Security: bearer
POST/api/v1/directory-groups
POST /api/directory-groups — create a group. The group grants nothing until
a role is mapped onto it.
Request body
CreateDirectoryGroupRequest
Responses
| Status | Description |
|---|
200 | |
409 | name or external id already in use, or idempotency key reused |
422 | validation failure |
Security: bearer
GET/api/v1/directory-groups/{id}
GET /api/directory-groups/{id} — the group with its members and role
mappings (manager+).
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
Responses
| Status | Description |
|---|
200 | |
404 | group not visible in scope |
Security: bearer
PATCH/api/v1/directory-groups/{id}
PATCH /api/directory-groups/{id} — rename, re-describe, or re-point the
external id. Absent fields keep their values.
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
Request body
UpdateDirectoryGroupRequest
Responses
| Status | Description |
|---|
200 | |
404 | group not visible in scope |
409 | name or external id already in use, version conflict, or idempotency key reused |
422 | validation failure |
Security: bearer
DELETE/api/v1/directory-groups/{id}
DELETE /api/directory-groups/{id} — remove the group. Its memberships and
role mappings go with it, so every member loses exactly what this group
granted.
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
Responses
| Status | Description |
|---|
200 | |
404 | group not visible in scope |
Security: bearer
PUT/api/v1/directory-groups/{id}/members/{userId}
PUT /api/directory-groups/{id}/members/{userId} — put a user in the group.
Idempotent; a repeated call reports the same membership and emits nothing.
The ceiling applies to the insert and not to the no-op: adding a person is
what turns the roles this group already carries into that person's grants,
so every mapped role must be inside the caller's own set. A repeat of a
membership that already exists confers nothing new, which is why it stays a
200 — refusing it would break the documented idempotency a sync depends on
while closing no hole.
The 403 the ceiling raises is not added to the `utoipa` response list here:
the OpenAPI pins are a reviewed artifact, and this lane changes behaviour
rather than the documented contract. The next sanctioned re-pin should add
it to this operation and to `map_role`.
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
userId | path | string | Yes | |
Responses
| Status | Description |
|---|
200 | |
404 | group or user not visible in scope |
Security: bearer
DELETE/api/v1/directory-groups/{id}/members/{userId}
DELETE /api/directory-groups/{id}/members/{userId} — take a user out of the
group. The roles that group carried for them are gone; every other grant
survives.
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
userId | path | string | Yes | |
Responses
| Status | Description |
|---|
200 | |
404 | group or membership not visible in scope |
Security: bearer
PUT/api/v1/directory-groups/{id}/roles/{roleId}
PUT /api/directory-groups/{id}/roles/{roleId} — map a role onto the group.
Every current and future member holds it from now on (additive union with
their directly assigned roles). Idempotent.
The ceiling applies to the mapping write itself: the role's resolved
permission set must be inside the caller's own, or the mapping is a typed
403 and no row is written. A no-op repeat of an existing mapping confers
nothing new, so it stays a 200 for the same reason a repeated member add
does.
The 403 stays out of the `utoipa` response list for the reason given on
`add_member`: the pins are a reviewed artifact and this lane does not
re-pin them.
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
roleId | path | string | Yes | |
Responses
| Status | Description |
|---|
200 | |
404 | group or role not visible in scope |
Security: bearer
DELETE/api/v1/directory-groups/{id}/roles/{roleId}
DELETE /api/directory-groups/{id}/roles/{roleId} — drop a role from the
group. Members lose exactly that role where the group was its only source.
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
roleId | path | string | Yes | |
Responses
| Status | Description |
|---|
200 | |
404 | group or mapping not visible in scope |
Security: bearer
GET/api/v1/org/users
GET /api/org/users — the staff directory (manager+).
Parameters
| Name | In | Type | Required | Description |
|---|
limit | query | — | No | Page size (default 50, max 200). |
offset | query | — | No | Offset paging. |
Responses
| Status | Description |
|---|
200 | |
403 | requires manager role |
Security: bearer
POST/api/v1/org/users
POST /api/org/users — invite (create) a staff user. The row lands
INACTIVE with an unusable password; the response carries a one-time
setup token the invitee redeems at `POST /org/users/accept-invite`.
Request body
InviteUserRequest
Responses
| Status | Description |
|---|
200 | |
409 | email already in org, or idempotency key reused |
Security: bearer
POST/api/v1/org/users/accept-invite
POST /api/org/users/accept-invite — public invite redemption: set the
first password and activate the account; returns a normal token pair.
The token is single-use and expiring; an unknown, used, or expired
token is an indistinguishable 404.
Request body
AcceptInviteRequest
Responses
| Status | Description |
|---|
200 | |
404 | invite unknown, already used, or expired |
422 | validation error |
POST/api/v1/org/users/{id}/email-verified
POST /api/org/users/{id}/email-verified — the admin OVERRIDE for the
email-verification gate (migration 0088). Audited both ways.
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
Request body
SetEmailVerifiedRequest
Responses
| Status | Description |
|---|
200 | |
403 | |
404 | |
Security: bearer
PATCH/api/v1/org/users/{id}/role
PATCH /api/org/users/{id}/role — assign a builtin org role. Each change
is audited; the last active owner cannot be demoted away from owner.
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
Request body
SetUserRoleRequest
Responses
| Status | Description |
|---|
200 | |
409 | last active owner, version conflict, or idempotency key reused |
Security: bearer
PATCH/api/v1/org/users/{id}/status
PATCH /api/org/users/{id}/status — activate or deactivate. Deactivation
revokes every live session and blocks login in the same commit.
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
Request body
SetUserStatusRequest
Responses
| Status | Description |
|---|
200 | |
409 | last active owner, version conflict, or idempotency key reused |
Security: bearer
GET/api/v1/roles
GET /api/roles — the org's roles plus the canonical preset + portal
tier rosters (manager+).
Responses
| Status | Description |
|---|
200 | |
403 | requires manage_users or manage_roles |
Security: bearer
PUT/api/v1/roles/{id}
PUT /api/roles/{id} — the custom-roles EDITOR (Wave 22): updates a
custom row's name and/or permission set. Builtin presets are typed
refusals (rb1) — restore is the ONLY preset-matrix mutation.
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
Request body
UpdateRoleRequest
Responses
| Status | Description |
|---|
200 | |
403 | builtin_role_immutable |
404 | |
422 | validation failure |
Security: bearer
POST/api/v1/roles/{id}/fork
POST /api/roles/{id}/fork — duplicate a role (typically a preset) into
a mutable custom copy on the same matrix. The origin row is untouched.
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
Request body
ForkRoleRequest
Responses
| Status | Description |
|---|
200 | |
404 | role not visible in scope |
409 | role name taken or idempotency key reused |
422 | validation failure |
Security: bearer
POST/api/v1/roles/{id}/restore
POST /api/roles/{id}/restore — reset a preset row's permission array to
the canonical shipped set (RB1: the presets stay canonical). Custom
roles and forks are org-owned data and are never restorable.
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
Request body
RestoreRoleRequest
Responses
| Status | Description |
|---|
200 | |
404 | role not visible in scope, or not a preset |
409 | version conflict or idempotency key reused |
Security: bearer