Roles and permissions
Authorization has two layers that are easy to confuse. Roles belong to users and travel in their tokens. Scopes belong to API keys and bound what a machine caller may do. Both draw from one permission set.
The permission set
Section titled “The permission set”Twenty-one permissions, defined centrally rather than inline at each route:
| Group | Permissions |
|---|---|
| Organization | org:read, org:write |
| Members | members:read, members:write |
| Credentials | credentials:read, credentials:issue, credentials:revoke |
| Audit | audit:read |
| Webhooks | webhooks:read, webhooks:write |
| API keys | apikeys:read, apikeys:write |
| Agents | agents:read, agents:write |
| SSO | sso:read, sso:write |
| Log streams | logstreams:read, logstreams:write |
| Connected accounts | connect:read, connect:write, connect:tokens |
* is a wildcard meaning every permission, present and future.
Note that connect:tokens is separate from connect:write. Retrieving a live third-party access
token from the vault is a different act from configuring providers, and it is
the more sensitive one — so an agent’s key can be scoped to token retrieval alone, without the
ability to add a provider or disconnect an account.
Default roles
Section titled “Default roles”Every new organization gets two roles:
| Role | Permissions |
|---|---|
admin | * |
member | org:read, members:read |
member is deliberately close to read-only: seeing the org and the roster is not the same as being
able to issue credentials or mint keys, and making that explicit means a new member cannot quietly
inherit issuance rights.
Assign a role through the management API:
POST /v1/organizations/org_01J…/members/usr_…/roleAuthorization: Bearer sk_live_…
{ "role": "admin" }A caller cannot grant a role carrying permissions it does not itself hold. Attempting it returns:
{ "error": "insufficient_scope", "detail": "role grants permissions beyond the caller's" }Roles in tokens
Section titled “Roles in tokens”When a user authenticates and belongs to an org, their ID token and access token carry both the org and the role:
{ "sub": "usr_01J…", "org_id": "org_01J…", "roles": ["admin"], "scope": "openid profile"}Your application authorizes on roles; the platform authorizes its own /v1 surface on API-key
scopes. A user’s role does not grant access to /v1 — that always requires a key.
API-key scopes
Section titled “API-key scopes”A key is created with an explicit scope list, and two rules bound it.
No escalation. A key may only mint keys at or below its own privilege. Requesting a scope the
calling key does not hold returns 403 insufficient_scope. A key without * can never create a key
with *.
No unknown scopes. A scope string that is not a known permission returns 400 invalid_scope
rather than being stored. A typo fails at creation instead of producing a key that silently
authorizes nothing, and it stops a future permission name from being squatted today.
POST /v1/organizations/org_01J…/api_keysAuthorization: Bearer sk_live_…
{ "name": "issuer-service", "scopes": ["credentials:issue", "credentials:read"] }Omitting scopes copies the calling key’s own scopes, which is convenient and usually too broad —
name the permissions the service actually needs.
Org binding
Section titled “Org binding”A key is bound either to one organization or to the platform. An org-bound key that addresses
another org is refused with 403 forbidden even when it holds the right permission, and audit
queries from it are clamped to its own org regardless of the organization_id parameter passed.
This is enforced at every route, not by convention: the same guard checks the permission and the org binding together, so adding an endpoint without an org check is not something that can be forgotten in one place and remembered in another. See multi-tenancy for the rest of the isolation model.
Provisioned users and roles
Section titled “Provisioned users and roles”Users who arrive by SCIM or by SSO just-in-time provisioning are added
to the org with the member role.
Directory groups sync as groups — they are stored, listable, and patchable through
/scim/v2/Groups — but they do not currently confer a role. Promoting a provisioned user to admin
is an explicit grant through POST /v1/organizations/:orgId/members/:userId/role, whether you make
it from your own provisioning code or a human makes it in the console.