Organization roles 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 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
Section titled “List roles”/api/v1/orgs/{orgUlid}/rolesAny 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
Section titled “Create a role”/api/v1/orgs/{orgUlid}/rolesRequires 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 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
Section titled “Update a role”/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
Section titled “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
Section titled “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
Section titled “Delete a role”/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.
{ "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
Section titled “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
Section titled “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.