Tenants and members
These endpoints create and rename tenants, and manage membership, roles, and invitations. Unless an entry says otherwise, they require tenant administrator rights or tenant-scoped users-manage. See Tenants for the admin model.
Tenants
Create tenant
POST /api/v1/tenants
Creates a tenant and adds the caller as a member. Installation admin only. The server trims the name. The answer is 400 when the name is blank, and 409 when another tenant has it.
Request body:
{ "name": "Acme" }
Response: 201 Created with { "id": "...", "name": "Acme" }.
Rename tenant
PUT /api/v1/tenants/{tenant_id}
Changes the display name of the tenant. Requires tenant administrator rights: tenant-scoped users-manage is not enough. The name rules of Create tenant apply.
Request body:
{ "name": "Acme Platform" }
Response: 200 OK with { "id": "...", "name": "..." }.
Members
List members
GET /api/v1/tenants/{tenant_id}/members
Returns a page of the active members of the tenant, oldest membership first.
| Parameter | Type | Required | Description |
|---|---|---|---|
cursor |
query | No | Pagination cursor from a previous response's next_cursor |
limit |
query | No | Max results (default: 25) |
Response:
{
"members": [
{
"user_id": "f30ed1f9-be44-46a3-9050-03e9561e94f0",
"name": "Jane Doe",
"email": "jane@example.com",
"type": "user",
"joined_at": "2026-06-04T10:30:00Z",
"admin_scope": "tenant",
"users_manage_scope": "none"
}
],
"total_count": 1,
"limit": 25,
"has_more": false,
"next_cursor": null
}
admin_scope and users_manage_scope are each none, tenant, or wider (the grant reaches beyond this tenant, for example for an instance admin).
Add member
POST /api/v1/tenants/{tenant_id}/members
Adds an existing user as a plain member. Idempotent: an existing member returns unchanged with 200, and a new membership returns 201. The answer is 404 when no such active user exists. Invite a person who has no account.
Request body:
{ "user_id": "f30ed1f9-be44-46a3-9050-03e9561e94f0" }
Response: 200 OK or 201 Created with the member object.
Set member role
POST /api/v1/tenants/{tenant_id}/members/set-role?user_id={user_id}
Grants or revokes the tenant administrator and membership management rights of a member. The two are independent: omit a field to leave that right unchanged. tenant_admin: true requires tenant administrator rights. The answer is 409 when you demote yourself or the last administrator, or edit a member whose grant is wider than this tenant.
Request body:
{ "tenant_admin": true, "can_manage_users": true }
Response: 200 OK with the member object and the scopes actually stored.
Remove member
DELETE /api/v1/tenants/{tenant_id}/members?user_id={user_id}
Removes the membership and revokes the tenant-scoped grants of the user in one transaction. You cannot remove yourself or the last administrator of the tenant. The answer is 404 when the user is not a member, and 409 when the grant of the member is wider than this tenant.
Response: 200 OK with { "user_id": "...", "removed": true }.
Invitations
List invitations
GET /api/v1/tenants/{tenant_id}/invitations
| Parameter | Type | Required | Description |
|---|---|---|---|
cursor |
query | No | Pagination cursor from a previous response's next_cursor |
limit |
query | No | Max results (default: 25) |
Response: invitations, plus total_count, limit, has_more, and next_cursor. Each invitation has id, email, role (member or admin), status (pending, accepted, revoked, or expired), invited_by_name, created_at, expires_at, send_count, and delivered.
Create invitation
POST /api/v1/tenants/{tenant_id}/invitations
Creates an invitation and sends the email. Delivery is best-effort, so the response always has a copyable invite_url. tenant_admin: true requires tenant administrator rights. The answer is 409 when the address already has a live invitation (reissue it instead), and 429 when a rate limit is hit.
Request body:
{ "email": "john@example.com", "tenant_admin": false }
Response: 201 Created
{
"invitation": { "id": "...", "email": "john@example.com", "role": "member", "status": "pending", "...": "..." },
"invite_url": "https://stategraph.example.com/invitations/accept?token=...",
"delivery": "emailed"
}
delivery is one of emailed, not_configured, no_provider_key, inviter_has_no_email, or transport_failed.
Reissue invitation
POST /api/v1/tenants/{tenant_id}/invitations/reissue?id={invitation_id}
Makes a new token for a pending invitation, resets its lifetime and send budget, and sends the email again. The previous link stops working. The answer is 404 when the invitation is not this tenant's, 409 when it is no longer pending, and 429 during the send cooldown.
Response: 200 OK with the same shape as Create invitation.
Revoke invitation
POST /api/v1/tenants/{tenant_id}/invitations/revoke?id={invitation_id}
Invalidates a pending invitation, so that its link can no longer be accepted.
Response: 204 No Content
Preview invitation
GET /api/v1/invitations/preview?token={token}
Needs no authentication. Returns the public details of an invitation for its accept page: tenant_name, inviter_name, role, status, expires_at, and the masked recipient address invited_email_masked. The answer is 404 for an unknown token, 410 when it expired, and 409 when it was revoked or already accepted.
Accept invitation
POST /api/v1/invitations/accept
Joins the signed-in user to the tenant that the token names. Requires only a valid session. Idempotent for an existing member.
Request body:
{ "token": "..." }
Response:
{
"tenant_id": "550e8400-e29b-41d4-a716-446655440000",
"tenant_name": "Acme",
"already_member": false,
"email_mismatch": false
}