Skip to Content
API referenceTenant directory API

Tenant directory API

The directory API is rooted at /identity/directory relative to the exact tenant issuer. Its resource URI is that complete URL. Obtain an access token from the same issuer with audience equal to the resource URI.

ScopeEffect
directory.readread directory collections
directory.manageread and mutate directory data

The authenticated subject must also hold the corresponding live directory permission.

Security boundary

Directory objects express Identity-side authorization. They do not create or replace organizations, subscriptions, memberships or roles owned by a product backend.

All collection reads use the shared pagination contract. A write that returns 204 No Content succeeded and has no JSON body.

Organizations

Method and routeSuccess
GET /organizations200 page of organizations
POST /organizations201 generated ID
ISSUER='https://auth.example.com'
curl --fail --silent --show-error \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H 'Content-Type: application/json' \
  --data '{"slug":"engineering","displayName":"Engineering"}' \
  "$ISSUER/identity/directory/organizations"

Create response — 201 Created

{ "id": "2a546f88-e746-4cdf-a87b-300a236d4788" }

Organization entry

{ "id": "2a546f88-e746-4cdf-a87b-300a236d4788", "slug": "acme", "displayName": "Acme", "status": "ACTIVE" }

status is ACTIVE or DISABLED. Use slug, rather than id, in child route paths.

Teams

Method and routeSuccess
GET /organizations/{organizationSlug}/teams200 page of teams
POST /organizations/{organizationSlug}/teams201 generated ID

Create a team with:

{ "slug": "support", "displayName": "Customer support" }

A team entry contains id, organizationId, slug, displayName and status.

Memberships

Method and routeSuccess
GET /organizations/{organizationSlug}/members200 page of members
POST /organizations/{organizationSlug}/members204 empty body
POST /organizations/{organizationSlug}/members/removals204 empty body
GET /organizations/{organizationSlug}/teams/{teamSlug}/members200 page of members
POST /organizations/{organizationSlug}/teams/{teamSlug}/members204 empty body
POST /organizations/{organizationSlug}/teams/{teamSlug}/members/removals204 empty body

Add and remove requests carry the user’s tenant-local subject:

{ "subject": "usr_01JY7N8SV4B30TQGB8Y3YH5J9M" }

A member entry is:

{ "subject": "usr_01JY7N8SV4B30TQGB8Y3YH5J9M", "status": "ACTIVE" }

Adding a team member requires the organization membership to be valid. Removing an organization member also removes the access relationships that depend on that membership.

Permissions and roles

Method and routeSuccess
GET /permissions200 page of permissions
POST /permissions201 empty body
GET /roles200 page of roles
POST /roles201 generated ID
GET /roles/{roleKey}/permissions200 page of permissions
POST /roles/{roleKey}/permissions204 empty body
DELETE /roles/{roleKey}/permissions/{permissionKey}204 empty body

Create a permission:

{ "key": "orders.refund", "displayName": "Refund orders" }

A permission entry returns key, displayName and status.

Create a role:

{ "key": "support-agent", "displayName": "Support agent", "scope": "ORGANIZATION" }

A role entry returns id, key, displayName, scope and status. Scope is TENANT, ORGANIZATION or TEAM.

Add a permission to a role:

{ "key": "orders.refund" }

Role assignments

Method and routeSuccess
GET /role-assignments200 page of assignments
POST /role-assignments204 empty body
POST /role-assignments/revocations204 empty body

The assignment and revocation body has the same shape. The required context IDs depend on scope.

Tenant scope

{ "roleKey": "tenant-auditor", "subject": "usr_01JY7N8SV4B30TQGB8Y3YH5J9M", "scope": "TENANT", "organizationId": null, "teamId": null }

Organization scope

{ "roleKey": "support-agent", "subject": "usr_01JY7N8SV4B30TQGB8Y3YH5J9M", "scope": "ORGANIZATION", "organizationId": "2a546f88-e746-4cdf-a87b-300a236d4788", "teamId": null }

Team scope

{ "roleKey": "team-lead", "subject": "usr_01JY7N8SV4B30TQGB8Y3YH5J9M", "scope": "TEAM", "organizationId": "2a546f88-e746-4cdf-a87b-300a236d4788", "teamId": "ea0f01bc-a1ce-46e1-a06e-e817e97a48df" }

An assignment entry returns the same five fields. Tenant assignments require both IDs to be null; organization assignments require organizationId and a null teamId; team assignments require both IDs.

Errors

Directory errors use {"error":"code"}.

StatusErrorMeaning and client action
400invalid_requestfix pagination, path values, JSON shape, slug, key, subject or scope context
401invalid_tokenobtain a live token from this tenant issuer for the directory resource
403insufficient_scoperequest the scope named by WWW-Authenticate
403access_deniedgrant the subject the corresponding live directory permission
404unknown_tenantthe request issuer does not identify an active tenant
409directory_operation_rejecteda referenced organization, team, role, permission or subject is absent, already linked, or incompatible with the requested state

A rejected mutation is safe to investigate with the relevant collection read before retrying. Do not treat every 409 as success: the code intentionally covers both duplicate relationships and missing dependencies.