Skip to content

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).

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:

FieldNotes
ulidServer-assigned identifier
slugThe role’s stable identifier — what role fields elsewhere reference
nameDisplay name
descriptionOptional, or null
isSystemtrue for one of the four fixed roles
isDefaulttrue for the organization’s default role (see below)
permissionsThe role’s effective permission names
memberCountCount of accepted memberships currently holding this role’s slug (pending invitations are not counted)

Alongside items, the response envelope also carries:

FieldNotes
customRolesEnabledWhether 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
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 below.

FieldRequiredNotes
slugyesLowercase, digits and hyphens, 1–63 characters. Cannot be owner, admin, member, or viewer
nameyes1–80 characters
descriptionnoUp to 500 characters
permissionsyesArray of catalog permission names — may be empty
isDefaultnoSet 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.

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.

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.

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/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.

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.