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.
| Scope | Effect |
|---|---|
directory.read | read directory collections |
directory.manage | read and mutate directory data |
The authenticated subject must also hold the corresponding live directory permission.
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 route | Success |
|---|---|
GET /organizations | 200 page of organizations |
POST /organizations | 201 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 route | Success |
|---|---|
GET /organizations/{organizationSlug}/teams | 200 page of teams |
POST /organizations/{organizationSlug}/teams | 201 generated ID |
Create a team with:
{
"slug": "support",
"displayName": "Customer support"
}A team entry contains id, organizationId, slug, displayName and status.
Memberships
| Method and route | Success |
|---|---|
GET /organizations/{organizationSlug}/members | 200 page of members |
POST /organizations/{organizationSlug}/members | 204 empty body |
POST /organizations/{organizationSlug}/members/removals | 204 empty body |
GET /organizations/{organizationSlug}/teams/{teamSlug}/members | 200 page of members |
POST /organizations/{organizationSlug}/teams/{teamSlug}/members | 204 empty body |
POST /organizations/{organizationSlug}/teams/{teamSlug}/members/removals | 204 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 route | Success |
|---|---|
GET /permissions | 200 page of permissions |
POST /permissions | 201 empty body |
GET /roles | 200 page of roles |
POST /roles | 201 generated ID |
GET /roles/{roleKey}/permissions | 200 page of permissions |
POST /roles/{roleKey}/permissions | 204 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 route | Success |
|---|---|
GET /role-assignments | 200 page of assignments |
POST /role-assignments | 204 empty body |
POST /role-assignments/revocations | 204 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"}.
| Status | Error | Meaning and client action |
|---|---|---|
400 | invalid_request | fix pagination, path values, JSON shape, slug, key, subject or scope context |
401 | invalid_token | obtain a live token from this tenant issuer for the directory resource |
403 | insufficient_scope | request the scope named by WWW-Authenticate |
403 | access_denied | grant the subject the corresponding live directory permission |
404 | unknown_tenant | the request issuer does not identify an active tenant |
409 | directory_operation_rejected | a 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.