# Organization roles API

> List, create, update, and delete an organization's authorization roles over the integrator API.

An organization's roles determine what a member can do. Every organization always has four **system
roles** — `owner`, `admin`, `member`, `viewer` — whose permission bundles ship with the product and
cannot be changed. An organization on a plan that includes custom roles can also define its own
**custom roles**, each with its own name and permission set, and assign them to members the same way
it assigns a system role.

A role's `role` field on the wire is always a plain slug string — the four system roles use their
fixed names (`owner`, `admin`, `member`, `viewer`); a custom role's slug is chosen at creation and
cannot be changed afterward (see [Renaming a role](#renaming-a-role) below).

The system bundles are fixed by the product: `viewer` holds **no** catalog permission — every
mutating API endpoint refuses it with `403 forbidden`, while its read access is unchanged because
reads are authorized by project access, not by role. `member` holds the read/write bundle for
day-to-day testing work; `admin` and `owner` hold the full bundle, including the permissions that
gate organization-level and project-level management (for example `projects.manage`, which `POST
/api/v1/projects` requires — a `member` cannot create a project).

## List roles

`GET /api/v1/orgs/{orgUlid}/roles`

Any organization member may call this endpoint — reading the list only requires membership, not any
particular permission or plan feature, because a client needs it to render a role picker. The response
is a single, unpaginated page (an organization holds at most 54 roles: four system roles plus a cap of
50 custom roles) containing an `items` array of every role, system and custom, each with:

| Field         | Notes                                                                                                  |
| ------------- | ------------------------------------------------------------------------------------------------------ |
| `ulid`        | Server-assigned identifier                                                                             |
| `slug`        | The role's stable identifier — what `role` fields elsewhere reference                                  |
| `name`        | Display name                                                                                           |
| `description` | Optional, or `null`                                                                                    |
| `isSystem`    | `true` for one of the four fixed roles                                                                 |
| `isDefault`   | `true` for the organization's default role (see below)                                                 |
| `permissions` | The role's effective permission names                                                                  |
| `memberCount` | Count of accepted memberships currently holding this role's slug (pending invitations are not counted) |

Alongside `items`, the response envelope also carries:

| Field                | Notes                                                                                                                                                                     |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `customRolesEnabled` | Whether this organization's plan includes custom roles — a plan fact about the caller's own organization, present on this read regardless of the caller's own permissions |

## Create a role

`POST /api/v1/orgs/{orgUlid}/roles`

Requires the organization role-management permission (held by `owner`/`admin`) **and** the
organization's plan to include custom roles. Both conditions must hold; see
[Two gates, one refusal](#two-gates-one-refusal) below.

| Field         | Required | Notes                                                                                             |
| ------------- | -------- | ------------------------------------------------------------------------------------------------- |
| `slug`        | yes      | Lowercase, digits and hyphens, 1–63 characters. Cannot be `owner`, `admin`, `member`, or `viewer` |
| `name`        | yes      | 1–80 characters                                                                                   |
| `description` | no       | Up to 500 characters                                                                              |
| `permissions` | yes      | Array of catalog permission names — may be empty                                                  |
| `isDefault`   | no       | Set this role as the organization's default (see below)                                           |

A duplicate slug within the organization, or one of the four reserved slugs, returns `409 conflict`.
An organization already holding 50 custom roles receives `409 conflict` on the 51st.

## Update a role

`PATCH /api/v1/orgs/{orgUlid}/roles/{roleUlid}`

Same gate as create. A custom role accepts `name`, `description`, `permissions`, and `isDefault`. A
system role accepts **only** `isDefault` — any other field returns `403 forbidden` and changes
nothing, because a system role's permission bundle is fixed by the product, not by data.

### Renaming a role

**A role's `slug` cannot be changed after creation.** A request carrying a `slug` key is refused
`422 validation_failed`, regardless of its value. To rename a role, send its new `name` — the display
name is the field a "rename" actually means. Every member already assigned the role keeps working
unaffected; only the label changes.

### The default role

Exactly one role in an organization is the default — the role a client may pre-select when inviting
someone, and the fallback target when a role is deleted without naming a replacement (see below). The
default starts as `viewer`. Setting `isDefault: true` on another role moves the default there
atomically; the previous default's flag clears in the same request. Clearing the only default (setting
`isDefault: false` on it with nothing else becoming default) returns `409 conflict` — an organization
always has exactly one default.

## Delete a role

`DELETE /api/v1/orgs/{orgUlid}/roles/{roleUlid}`

Same gate as create. Only a custom role can be deleted — a system role, or the organization's current
default role, returns `403`/`409` respectively. The response is `200` with a JSON body (never `204`),
because the body's exact counts are what let a retried request replay instead of repeating the
operation.

```json
{ "reassignedTo": "member", "reassignedMemberCount": 3 }
```

Every member and pending invitation holding the deleted role moves to `reassignTo` — named explicitly
in the request body, or the organization's default role if omitted.

### The omitted-target fallback can refuse a delete you expect to succeed

**Omitting `reassignTo` is not always accepted.** It falls back to the organization's default role
only when that default's permissions are a subset of the role being deleted — moving members to a
"smaller" role is always safe, moving them to a role that grants something new is not something the
API does silently. If the default does not qualify, the request is refused
`422 validation_failed`, naming `reassignTo` as the field to supply explicitly. This never removes a
capability: naming the target explicitly in the request always works, because you are the one making
the explicit choice. It only prevents a role's members from silently gaining permissions because no
target was named.

## Two gates, one refusal

Creating, updating, and deleting a custom role both require the role-management permission **and**
an entitlement on the organization's plan. **A `403 forbidden` from any of these three endpoints never
reveals which of the two was missing** — the permission or the plan feature. This is deliberate: a
distinguishable refusal would let any caller probe which organizations are on a plan that includes
custom roles. If you receive a `403` here and you believe you hold the right organization role, check
with your organization owner whether the plan includes custom roles before assuming the API is wrong.

Reading the role list (`GET`) is never gated by the plan — every organization, on every plan, can
always see its four system roles and any custom roles it already has.
