Organizations API
Organizations are company-level entities that own and group workspaces, and they are where all access control lives: people, groups, roles and the grants that connect them.
Every workspace belongs to exactly one organization. A group is granted one or more roles on each workspace it should reach, and that grant is the only thing that grants access — there is no workspace membership record and no workspace invitation.
Endpoints
| Method | Path | Description |
|---|---|---|
GET | /api/v1/organizations | List organizations |
POST | /api/v1/organizations | Create an organization |
GET | /api/v1/organizations/{orgId} | Get an organization |
PUT | /api/v1/organizations/{orgId} | Update an organization |
DELETE | /api/v1/organizations/{orgId} | Delete an organization |
GET | /api/v1/organizations/{orgId}/workspaces | List org workspaces |
POST | /api/v1/organizations/{orgId}/workspaces | Create workspace in org |
GET | /api/v1/organizations/{orgId}/groups | List groups |
POST | /api/v1/organizations/{orgId}/groups | Create a group |
GET | /api/v1/organizations/{orgId}/groups/{groupId} | Get a group |
PUT | /api/v1/organizations/{orgId}/groups/{groupId} | Update a group |
DELETE | /api/v1/organizations/{orgId}/groups/{groupId} | Delete a group |
GET | /api/v1/organizations/{orgId}/groups/{groupId}/members | List a group’s members |
POST | /api/v1/organizations/{orgId}/groups/{groupId}/members | Add an account to a group |
DELETE | /api/v1/organizations/{orgId}/groups/{groupId}/members/{accountId} | Remove one |
PUT | /api/v1/organizations/{orgId}/groups/{groupId}/workspaces/{workspaceId} | The grant — set the roles this group holds on this workspace |
DELETE | /api/v1/organizations/{orgId}/groups/{groupId}/workspaces/{workspaceId} | Revoke it |
GET | /api/v1/organizations/{orgId}/roles | List built-in + custom roles |
POST | /api/v1/organizations/{orgId}/roles | Create a custom role |
PUT | /api/v1/organizations/{orgId}/roles/{roleId} | Update one |
DELETE | /api/v1/organizations/{orgId}/roles/{roleId} | Delete one |
GET | /api/v1/organizations/{orgId}/members | List org members |
POST | /api/v1/organizations/{orgId}/members/invite | Invite a new member by email |
DELETE | /api/v1/organizations/{orgId}/members/{accountId} | Remove a member |
GET | /api/v1/organizations/{orgId}/invites | List pending invitations |
DELETE | /api/v1/organizations/{orgId}/invites/{inviteId} | Cancel an invitation |
POST | /api/v1/organizations/{orgId}/invites/{inviteId}/accept | Accept an invitation |
POST | /api/v1/organizations/{orgId}/invites/{inviteId}/resend | Resend an invitation email |
List Organizations
GET /api/v1/organizations
Returns all organizations the authenticated user is a member of — or, for a platform administrator, every organization on the platform. Does not require the X-Workspace-ID header.
Each entry carries caller_is_admin: whether you are in that organization’s Organization admins group, and so may create workspaces in it. It is true on every entry for a platform administrator. Only this list fills it in; the other organization reads return it as false.
Response
[
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Acme Corp",
"slug": "acme-corp",
"avatar_url": "",
"settings": {},
"created_by": "660e8400-e29b-41d4-a716-446655440000",
"workspace_count": 3,
"member_count": 15,
"caller_is_admin": true,
"created_at": "2024-01-01T00:00:00Z",
"updated_at": "2024-01-15T09:30:00Z"
}
]Example
curl -X GET https://agentic.zeotap.com/api/v1/organizations \
-H "Authorization: Bearer <token>"Create Organization
POST /api/v1/organizations
Creates a new organization. The organization is seeded with its Organization admins and Organization viewers groups, and the authenticated user is put in Organization admins. Any approved account may create one; an account whose access has not been approved yet is refused 403 access_required.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Organization display name. The organization’s slug is derived from it |
{
"name": "Acme Corp"
}Response
Returns the created organization with status 201 Created.
Errors
| Status | Code | Meaning |
|---|---|---|
400 | organization_slug_required | The name contains no letter or digit to derive an identifier from |
403 | access_required | Your account’s access request has not been approved |
409 | organization_slug_taken | Another organization already has the identifier this name produces. Choose a different name |
Example
curl -X POST https://agentic.zeotap.com/api/v1/organizations \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"name": "Acme Corp"
}'Get Organization
GET /api/v1/organizations/{orgId}
Returns a single organization by ID.
Update Organization
PUT /api/v1/organizations/{orgId}
Updates organization details. Requires membership of the organization’s Organization admins group.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
name | string | No | Updated display name. Renaming also re-derives the slug, and is refused 409 organization_slug_taken if another organization already holds the new one |
avatar_url | string | No | Updated avatar URL |
settings | object | No | Updated org settings |
Delete Organization
DELETE /api/v1/organizations/{orgId}
Deletes an organization. Requires membership of the Organization admins group.
Refused 409 organization_has_workspaces while the organization still holds any workspace, and 409 organization_has_hierarchies while any of its workspaces is in a hierarchy. Move the workspaces to another organization first — there is no cascade, because deleting an organization is not a request to destroy what is inside it.
Response
{
"status": "deleted"
}Workspace Management
List Org Workspaces
GET /api/v1/organizations/{orgId}/workspaces
Returns all workspaces belonging to the organization.
Create Workspace in Org
POST /api/v1/organizations/{orgId}/workspaces
Creates a new workspace under the organization. This is the only way to create a workspace, and it requires organization administration — membership of the Organization admins group, or platform administration. Returns the workspace with status 201 Created.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Workspace display name |
slug | string | No | URL-safe identifier, unique across the platform. Stored as sent; refused 409 workspace_slug_taken if another workspace already uses it. Omit it to have one derived from the name and made unique |
{
"name": "Production",
"slug": "production"
}Errors
| Status | Code | Meaning |
|---|---|---|
403 | access_required | Your account’s access request has not been approved |
403 | — | You are not an administrator of this organization |
409 | workspace_slug_taken | The slug you sent is already used by another workspace |
Retired: POST /api/v1/workspaces
POST /api/v1/workspaces no longer creates workspaces. It answers 410 Gone with the code workspace_creation_moved, naming the organization route above. An account that belongs to no organization creates one first with POST /api/v1/organizations.
Groups and Grants
A group belongs to an organization, holds accounts, and is granted one or more roles on each workspace it should reach.
Every organization is seeded with two groups carrying a system_key — org_admins and org_viewers — which cannot be renamed (409 system_group_immutable) or deleted. Each holds one wildcard grant: a row with no workspace_id, meaning every workspace in the organization, now and in future. Membership of org_admins is what organization administration is; there is no role on the roster.
List Groups
GET /api/v1/organizations/{orgId}/groups
{
"groups": [
{
"id": "7c1f4e2a-9b3d-4f18-a0c6-2d5e8b1f9034",
"organization_id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Organization admins",
"description": "Administers this organization and every workspace in it",
"system_key": "org_admins",
"is_system": true,
"member_count": 2,
"created_at": "2024-01-01T00:00:00Z",
"updated_at": "2024-01-01T00:00:00Z"
}
]
}Create, Update, Delete a Group
POST /api/v1/organizations/{orgId}/groups takes {"name", "description"?}. Names are unique within the organization, compared without regard to case.
PUT …/groups/{groupId} takes the same fields. DELETE …/groups/{groupId} removes the group, its members, its grants and its access-policy assignments. Both refuse 409 system_group_immutable on a seeded group.
All three require organization administration.
Group Membership
POST /api/v1/organizations/{orgId}/groups/{groupId}/members takes {"account_id"}; the account must already be in the organization. DELETE …/members/{accountId} removes them.
Removing the last member of org_admins is refused 409 last_org_admin. The rule is a transition, not a state — with two administrators either removal is allowed — so an empty admins group is a legitimate state (a platform administrator can create an organization for a customer who has not joined yet).
Set a Grant
PUT /api/v1/organizations/{orgId}/groups/{groupId}/workspaces/{workspaceId}
States the complete set of roles the group holds on this workspace. A role the group holds and the body omits is dropped, so this both grants and revokes.
| Field | Type | Required | Description |
|---|---|---|---|
role_ids | array of string (UUID) | Yes | Every role this group should hold on this workspace. An empty array attaches the group and grants no permissions — its members count as members of the workspace, which keeps its access policies meaningful, and can do nothing |
role_id | string (UUID) or null | No | The single-role spelling this endpoint shipped with, still accepted. role_ids wins when both are present |
A group may hold several roles on one workspace, and what it grants there is the UNION of their permissions. Nothing subtracts, so adding a role can only widen what the group’s members can do — worth knowing, because “Viewer and Editor” reads as though it might mean the narrower of the two. This is how “Viewer everywhere, plus Editor on analytics” is expressed without inventing a role per combination.
The response is {"assignments": [...]}, one entry per role.
The workspace must be in the same organization as the group; the database enforces it.
Revoke a Grant
DELETE /api/v1/organizations/{orgId}/groups/{groupId}/workspaces/{workspaceId}
Removes every role the group holds on that workspace, and its access-policy assignments there. Distinct from PUT with an empty role_ids, which leaves the group attached granting nothing.
Read the Grants Reaching One Workspace
GET /api/v1/workspaces/{id}/groups — the workspace-side read, backing the workspace Groups tab. Each assignment carries group_id, group_name, group_system_key, workspace_id (absent on a wildcard) and role_id. There is one entry per role, so a group holding two roles on the workspace appears twice; group them by group_id to show a row per group.
Roles
Custom roles belong to the organization, so one role is written once and can be granted in every workspace in it. The three built-in roles belong to no organization, which is what makes the same three ids usable everywhere:
| Role | ID |
|---|---|
| Owner | 00000000-0000-0000-0000-000000000001 |
| Admin | 00000000-0000-0000-0000-000000000002 |
| Member | 00000000-0000-0000-0000-000000000003 |
GET /api/v1/organizations/{orgId}/roles lists both sets. POST, PUT and DELETE manage the custom ones and require roles.write.
A role still held by any grant cannot be deleted: the delete first moves every such grant to the built-in Member role, and refuses if that cannot be done. Without it, the ON DELETE CASCADE on the grant’s role_id would delete the grants instead, revoking a group’s access with no error anywhere.
Member Management
The organization roster answers one question — does this account belong to this organization — and carries no role. What somebody can do comes from the groups they are in.
List Members
GET /api/v1/organizations/{orgId}/members
[
{
"organization_id": "550e8400-e29b-41d4-a716-446655440000",
"account_id": "660e8400-e29b-41d4-a716-446655440000",
"email": "alice@example.com",
"name": "Alice Smith",
"avatar_url": "https://...",
"groups": [
{ "group_id": "7c1f…", "group_name": "Organization admins", "system_key": "org_admins" }
],
"created_at": "2024-01-01T00:00:00Z"
}
]Invite Member
POST /api/v1/organizations/{orgId}/members/invite
Sends an email invitation to add a new member to the organization. The new account is created on acceptance.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
email | string | Yes | Email address of the user to invite |
group_ids | array of string (UUID) | No | The groups the invitee lands in on acceptance. An empty list is legal and means they join the roster and reach nothing until a group takes them in |
Remove Member
DELETE /api/v1/organizations/{orgId}/members/{accountId}
Removes a member from the organization, and with them their membership of every group in it — and therefore their access to every workspace those groups reached. Nothing survives it, because there is no workspace membership record to survive.
Invitations
Invitations are managed via the members/invite endpoint (see Invite Member above) and the invites collection below.
List Invitations
GET /api/v1/organizations/{orgId}/invites
Returns all pending invitations.
Response
[
{
"id": "770e8400-e29b-41d4-a716-446655440000",
"organization_id": "550e8400-e29b-41d4-a716-446655440000",
"email": "newuser@example.com",
"groups": [
{ "group_id": "7c1f4e2a-9b3d-4f18-a0c6-2d5e8b1f9034", "group_name": "EMEA Marketing" }
],
"invited_by": "660e8400-e29b-41d4-a716-446655440000",
"inviter_name": "Alice Smith",
"status": "pending",
"expires_at": "2024-02-15T00:00:00Z",
"created_at": "2024-01-15T09:30:00Z",
"updated_at": "2024-01-15T09:30:00Z"
}
]Cancel Invitation
DELETE /api/v1/organizations/{orgId}/invites/{inviteId}
Cancels a pending invitation.
Accept Invitation
POST /api/v1/organizations/{orgId}/invites/{inviteId}/accept
Accepts an invitation. The authenticated user is added to the organization and to the groups the invitation named.
Idempotent: the post-signup verification flow can land two tabs on the accept page at once, and the loser of that race sees success rather than a refusal, for an invitation that did go through.
Resend Invitation
POST /api/v1/organizations/{orgId}/invites/{inviteId}/resend
Resends the invitation email for a pending invite.
Organization Object
| Field | Type | Description |
|---|---|---|
id | string (UUID) | Unique identifier |
name | string | Display name |
slug | string | URL-safe identifier |
avatar_url | string | Avatar image URL |
settings | object | Organization settings |
created_by | string (UUID) | Account that created the org |
workspace_count | integer | Number of workspaces |
member_count | integer | Number of members |
caller_is_admin | boolean | Whether you administer this organization. Filled in only by List Organizations; false elsewhere |
created_at | string (ISO 8601) | Creation timestamp |
updated_at | string (ISO 8601) | Last update timestamp |
Organization Member Object
| Field | Type | Description |
|---|---|---|
organization_id | string (UUID) | Organization ID |
account_id | string (UUID) | Account ID |
groups | array | The groups this account is in — {group_id, group_name, system_key} each. There is no role: membership of the org_admins group is what organization administration is |
email | string | Member email |
name | string | Member display name |
avatar_url | string | Member avatar URL |
created_at | string (ISO 8601) | When the member was added |
Organization Invite Object
| Field | Type | Description |
|---|---|---|
id | string (UUID) | Unique identifier |
organization_id | string (UUID) | Organization ID |
email | string | Invited email address |
groups | array | The groups the invitee lands in on acceptance — {group_id, group_name} each. May be empty |
invited_by | string (UUID) | Account that sent the invite |
inviter_name | string | Name of the inviter |
status | string | pending, accepted, or cancelled |
expires_at | string (ISO 8601) | Seven days after creation. Expiry is a timestamp, not a status — a lapsed invite stays pending and is refused on accept |
created_at | string (ISO 8601) | Creation timestamp |
updated_at | string (ISO 8601) | Last update timestamp |