Groups API
The groups API manages user groups and group-based access grants on knowledge spaces (projects). Assigning access at the group level (rather than per user) is the foundation for directory-driven provisioning and role mapping.
All endpoints require an authenticated admin session (aig_admin cookie). The group
endpoints additionally require the ki_manager, tenant_admin or admin role.
The two org-share endpoints at the bottom of this page are the exception: any project owner may use them, including a plain member. The base URL is
https://ai-api-admin.myra.eu/admin/v1.
A ki_manager (the three-tier "Fachbereich" role between member and
tenant_admin) may create groups and read all groups in its tenant, but may
mutate (rename, delete, change membership, grant/revoke) only the groups it
created (created_by). tenant_admin and admin manage all groups in scope.
This keeps a ki_manager from churning the membership of a group it does not own —
which would confer/revoke access on resources granted to that group.
Every group, membership, and grant is tenant-scoped: a tenant_admin only
ever sees and mutates groups in their own tenant. A platform admin may operate
across tenants by passing an explicit tenant_id on create/list. A reference to a
resource (group id, user, project) in another tenant is rejected with 404 — the
API does not confirm the existence of out-of-tenant resources.
Group grants resolve through membership: a project's effective role for a user
is MAX(direct member role, best group-grant role) (owner > editor >
viewer). Removing a user from a group immediately revokes the group-derived
access; it never downgrades a higher direct role.
Every create / update / delete / grant / revoke is written to the audit log.
Groups
List groups
GET /admin/v1/groups
Returns the groups in the caller's tenant. Each row carries id, tenant_id, name,
description, created_by, created_at and updated_at (Unix seconds), plus a
member_count. A platform admin must pass ?tenant_id=<ID>; the request returns 400
if it is omitted. GET /admin/v1/groups/{id} returns the same fields and additionally
attaches the group's members and grants arrays.
Creating a group
POST /admin/v1/groups
| Field | Type | Required |
|---|---|---|
name |
string | yes — non-empty; unique per tenant. A clash returns 409. |
description |
string | no |
tenant_id |
string | only for a platform admin caller (400 if omitted by an admin). |
Returns 201 with { "id": "<group id>" }.
Get a group
GET /admin/v1/groups/{id}
Returns the group plus its members and grants arrays. A group in another
tenant returns 404.
Updating a group
PATCH /admin/v1/groups/{id}
Body { "name"?, "description"? }. A name clash returns 409; an empty name
returns 400.
Deleting a group
DELETE /admin/v1/groups/{id}
Hard-deletes the group; its memberships and grants are removed with it. Returns
200.
Members
List members
GET /admin/v1/groups/{id}/members
List add-member candidates
GET /admin/v1/groups/{id}/members/candidates
Returns the users of the group's tenant who are not already members — the
data source for the add-member picker's browsable list. Each entry is
{ id, email, name } (no role/login/status metadata). The server excludes, and
the client cannot broaden: soft-deleted users, platform admin users (a
non-admin caller cannot add an admin target, so they are never offered), and
existing members. The result is capped at 1000 rows (ordered by email);
anyone beyond the cap is still addable through the add-by-email path below.
Authorized like the add-member mutation, not the members-list read:
require_ki_manager and can_manage_group (admin ∨ tenant_admin ∨ the group's
creator). A ki_manager who does not manage the group → 403; a caller in another
tenant → 404 (no existence oracle). Because a group-managing ki_manager can read
this, the endpoint effectively exposes the tenant's non-admin user roster
(email + name) to that trusted intra-tenant tier — this is the intended, minimal
consequence of an in-tenant "browse existing members" picker.
Adding a member
POST /admin/v1/groups/{id}/members
| Field | Type | Required |
|---|---|---|
user_id |
string | yes — must belong to the same tenant as the group (cross-tenant or unknown user → 404). |
Returns 201. Re-adding an existing member is idempotent.
Removing a member
DELETE /admin/v1/groups/{id}/members/{user_id}
Idempotent — removing a non-member still returns 200.
Grants (group → resource)
A grant gives every member of the group a role on a resource. Creating a grant
additionally requires can_share: the caller must be the owner of the
resource, a tenant_admin, or a platform admin. A non-owner attempting to grant
a resource they do not own returns 403.
List a group's grants
GET /admin/v1/groups/{id}/grants
Creating a grant
POST /admin/v1/groups/{id}/grants
| Field | Type | Required |
|---|---|---|
resource_type |
project |
yes — any other value returns 400. (Agent grants are a planned addition.) |
resource_id |
string | yes — must be a resource in the same tenant as the group (cross-tenant or unknown → 404). |
role |
owner | editor | viewer |
yes — any other value returns 400. |
Returns 201. Re-granting the same group on the same resource updates the role
(idempotent). A group granted owner confers the resource's owner powers
(including deletion) on every member; group-conferred owners count toward the
resource's last-owner protection.
Revoke a grant
DELETE /admin/v1/groups/{id}/grants/{resource_type}/{resource_id}
Returns 200 (idempotent).
Rejected input summary
| Input | Rejected when | Status |
|---|---|---|
| any group route | caller is not ki_manager/tenant_admin/admin |
403 |
| org-share routes | caller is viewer/demouser, or has no/unknown role |
403 |
| org-share routes | caller is not the project owner (nor tenant_admin/admin) |
403 (same tenant) / 404 (cross-tenant or unknown) |
{id} |
group is in another tenant / unknown | 404 |
name |
empty, or duplicate within the tenant | 400 / 409 |
tenant_id |
omitted by a platform admin on create/list |
400 |
members.user_id |
unknown, or in a different tenant than the group | 404 |
grants.resource_type |
not project |
400 |
grants.resource_id |
unknown, or in a different tenant than the group | 404 |
grants.role |
not owner/editor/viewer |
400 |
| grant create/revoke | caller cannot can_share the resource (not owner, nor tenant_admin/admin) |
403/404 |
| any mutation | a ki_manager mutating a group it did not create |
403 |
Org-share
Org-share makes a project accessible to the whole tenant. It is a tenant
capability that an admin/tenant_admin can deactivate:
PATCH /admin/v1/tenants/{id} with { "org_share_enabled": 0 | 1 } (audited).
When the capability is 0, no new org-share can be created and every existing
org-share stops granting access at resolve time — retroactive, with no deny
rows. The toggle and a project's org_grant surface via GET /admin/auth/me
(tenant_org_share_enabled) and GET /admin/v1/projects/{id} (org_grant).
GET /admin/v1/projects/{id} additionally returns org_access — a
server-derived VIEW of whether the org grant actually resolves to access, for the
Members surface. It is { "role": "editor" | "viewer" } only when a grant row
exists and the tenant capability is on; it is absent otherwise. This is
distinct from org_grant (the raw grant row that drives the share/unshare toggle):
when the capability is switched off, the grant row — and thus org_grant — is
unchanged, but org_access disappears, because no tenant user has org access at
resolve time. The role in org_access is read from the same facts the access
resolver reads, so it can never claim access the resolver would not grant; a grant
that fails to resolve to a recognised role yields no org_access (fail closed).
Share a project with the organization
POST /admin/v1/projects/{id}/org-share — body { "role": "editor" | "viewer" }.
The admin UI sends viewer. Its control is a single toggle with no role picker, so
whatever it sends is what every project owner grants without being asked — and editor is
not a read role: it is the level that gates creating, uploading, moving and deleting a
project's knowledge files. A share meant to let colleagues see a project must not hand the
whole tenant delete rights on its documents. editor remains accepted for a deliberate,
API-driven share.
Gated by require_author AND can_share (owner ∨ tenant_admin ∨ admin) AND the
project's tenant org_share_enabled = 1.
This is available to any project owner, including a plain member — it is
no longer restricted to ki_manager/tenant_admin/admin. The reason is consistency with the
per-user path: POST /admin/v1/projects/{id}/members has always been owner-gated with no
manager-role requirement and accepts role: "owner", so an owner could already grant more,
one colleague at a time. require_author still refuses the read-only roles (viewer,
demouser) and fails closed on an absent or unrecognised role. Ownership may be direct or
inherited from a group that was granted the owner role on the project.
Role is capped at editor/viewer — never owner (an org-owner would make every tenant user
a project owner and could be laundered into a non-revocable group grant). Returns
403 org_share_disabled when the capability is off, 503 if the capability could not be read
because of a transient fault (safe to retry), 404 if the project's tenant no longer exists,
and 400 for role: "owner" or any non-editor/viewer value. A transient failure to read
the project itself is likewise a 503, never a misleading permanent 404. Idempotent (re-share updates the role — including upgrading an
existing viewer org grant to editor).
Stop sharing
DELETE /admin/v1/projects/{id}/org-share — gated by require_author AND can_share (but
not the capability flag, so cleanup works when it is off). Available to any project owner,
like sharing. Returns 200 (idempotent).
Role → capability matrix
ki_manager is the three-tier role between member and tenant_admin
("Fachbereich" manager). Least-privilege: it adds group management to member-level
capabilities, nothing more. (Org-share used to be part of that step up; it was moved to project ownership, so it is no longer what distinguishes a ki_manager.)
| Capability | admin | tenant_admin | ki_manager | member | finance | viewer | demouser |
|---|---|---|---|---|---|---|---|
| Manage groups (own / created) | all | all | own only | – | – | – | – |
| Org-share a project (owned) | yes | yes | yes | yes | – | – | – |
| Gateways / Users / Tenants / Providers / MCP admin | yes | yes (scoped) | – | – | – | – | – |
Analytics (/stats) scope |
tenant-wide | tenant-wide | own data | own data | own data | own data | own data |
| Create projects / agents, run inference | yes | yes | yes | yes | inference only | – | – |
| Assign roles | all | up to ki_manager | – | – | – | – | – |
finance is a cost-console role: it can run inference but cannot author (create projects, agents, or workflows) or administer anything. Its distinct surface is Settings → Costs → User Costs (/finance) and the tenant billing/cost console.
Scope: "Anwender" = the tenant / operating organization; org-share deactivation is tenant-scoped, enforced at grant-create AND at the resolver. The additive model has no per-individual opt-out. "Erweiterte Agentenkonfig" is delivered via agent sharing.
Superseded: an earlier reading additionally treated org-sharing as making "mit Org teilen" a
KI-Manager Fachbereich capability, and gated the endpoints accordingly so that a plain member
could not org-broadcast even a project they owned. Real user demand contradicted that reading —
a project owner with no manager role had no way to share their project and was adding members
one by one — so the gate is now project ownership plus the tenant capability flag. The
tenant-scoped deactivation requirement is unchanged and remains the admin control.
Note that agent org-share (api-reference/agents.md) is still ki_manager-gated, so the
two resources currently differ.