Team management¶
Authentication briefly covers logging in to get an admin session token. This page is the full reference: the complete login and profile surface, plus the full admin-user management API for inviting teammates, changing roles, and deactivating accounts.
Authentication¶
Every endpoint on this page takes an admin session token as a bearer
token (Authorization: Bearer <token>), except POST /api/v1/auth/login
itself. That endpoint takes no credential at all: it's how you obtain the
token in the first place. None of these endpoints accept an API key.
Log in: POST /api/v1/auth/login¶
No authentication required. Exchanges an admin's email and password for a session token.
Request¶
| Field | Type | Required | Description |
|---|---|---|---|
email |
string | Yes | Must be a syntactically valid email address. |
password |
string | Yes | No length or format constraint is enforced on login itself (constraints apply when the password is first set, see Invite an admin below). |
const response = await fetch("https://api.your-domain.com/api/v1/auth/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
email: "owner@client-domain.com",
password: "a-strong-password",
}),
});
const login = await response.json();
const adminToken = login.access_token;
A successful login returns 200 OK:
| Field | Type | Description |
|---|---|---|
access_token |
string | A JWT bearer token. Send it as Authorization: Bearer <access_token> on every subsequent admin request. |
token_type |
string | Always the literal string "bearer". |
expires_in |
integer | Token lifetime in seconds from the moment of this response. 8 hours (28800) by default; the deployment operator can change this. |
An unknown email, a wrong password, or a deactivated account (is_active
is false, see Deactivate an admin below) all
return the same 401 Unauthorized with {"detail": "Invalid email or
password"}. That's deliberate: it keeps a caller from using this
endpoint to enumerate which email addresses exist on the deployment.
There is no refresh-token flow and no server-side session table: the
token is a stateless, self-contained JWT. To stay logged in past
expires_in seconds, log in again for a fresh token. There's no way to
extend an existing one.
Get your own profile: GET /api/v1/auth/me¶
Requires a valid session token. Any role (Owner, Admin, Editor, or Viewer) can call this endpoint, and it returns only the caller's own record.
| Field | Type | Description |
|---|---|---|
id |
integer | This admin's id. |
email |
string | This admin's email address. |
role |
string | One of owner, admin, editor, viewer. See Roles below. |
is_active |
boolean | Always true when returned from this endpoint, a deactivated account can't authenticate at all, so it can never successfully call /me to see is_active: false about itself. |
Roles¶
AiFlow has four deployment-wide admin roles:
| Role | Capabilities |
|---|---|
owner |
Full control of the deployment, including creating, role-changing, and deactivating other admins, and the only role that can act on another Owner account or grant the Owner role. Also the only role permitted to call Data retention's deletion endpoint. |
admin |
Manages agents, tools, Triggers, and API keys. Can also call every endpoint on this page below (list, invite, change role, deactivate) for non-Owner accounts, see the note under Invite an admin. |
editor |
Can edit agent content, context documents, and Triggers (Triggers create/update/delete/test all require Editor or above). Cannot manage admin users, API keys, or webhook subscriptions. |
viewer |
Read-only: can view logs, dashboards, and call transcripts (Calls, Event log list/detail endpoints all permit Viewer), but cannot create, edit, delete, or test anything. |
owner and admin see and act on every agent in the deployment, always.
editor and viewer are scoped per agent: each needs an explicit
grant to a specific agent before they can see or touch it at all (see
Per-agent access below). This wasn't always true. A
deployment created before per-agent access shipped had every Editor or
Viewer see every agent, and upgrading preserved that: every Editor or
Viewer account was automatically granted every agent that existed at
upgrade time. Only agents created after upgrading require an explicit
grant.
List admin users: GET /api/v1/admin-users¶
Requires Owner or Admin. Returns every admin account on the deployment,
ordered by id, with no pagination.
[
{
"id": 1,
"email": "owner@client-domain.com",
"role": "owner",
"is_active": true
},
{
"id": 2,
"email": "support@client-domain.com",
"role": "viewer",
"is_active": true
}
]
Each entry has the same four fields documented under
GET /api/v1/auth/me above:
id (integer), email (string), role (string), is_active (boolean).
Invite an admin: POST /api/v1/admin-users¶
Requires Owner or Admin.
Request¶
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
email |
string | Yes | Must be a syntactically valid email address. Must not already belong to an existing admin. | |
password |
string | Yes | Minimum 8 characters. There is no server-side "temporary password, must change on first login" flow, whatever you set here is the account's real password until changed. | |
role |
string | Yes | (no default) | One of owner, admin, editor, viewer. There is no default; every invite must state a role. |
const response = await fetch("https://api.your-domain.com/api/v1/admin-users", {
method: "POST",
headers: {
Authorization: `Bearer ${adminToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
email: "support@client-domain.com",
password: "a-temporary-password",
role: "viewer",
}),
});
const newAdmin = await response.json();
Returns 201 Created with the new admin's id, email, role, and
is_active: true. An account is active immediately on creation: there's
no separate activation step.
Only an Owner can grant the Owner role
An Admin-role caller can invite a new admin, editor, or viewer
account through this endpoint: that's within the Owner-or-Admin bar
the route itself enforces. But if the request body's role is
"owner", the request is also checked against the caller's own role.
If the caller isn't an Owner, this returns 403 Forbidden with
{"detail": "Only an Owner can grant the Owner role"}, even though
the caller cleared the endpoint's base Owner-or-Admin requirement.
If email already belongs to an existing admin, this returns
409 Conflict with {"detail": "An admin with email '<email>' already
exists"}.
Change an admin's role: PATCH /api/v1/admin-users/{admin_user_id}/role¶
Requires Owner or Admin.
Request¶
| Field | Type | Required | Description |
|---|---|---|---|
role |
string | Yes | One of owner, admin, editor, viewer, the account's new role. |
Returns 200 OK with the updated admin record on success. admin_user_id
not matching any existing admin returns 404 Not Found.
Two guardrails apply, independent of the base Owner-or-Admin check:
- Owner involvement requires an Owner caller. If the target account's
current role is
owner, or the newrolebeing set isowner, the caller must be an Owner. Otherwise this returns403 Forbiddenwith{"detail": "Only an Owner can grant or modify the Owner role"}. An Admin can freely move an account betweenadmin,editor, andviewer, but cannot touch an Owner account in either direction. - The deployment can never be left with no active Owner or Admin.
Demoting someone to
editororvieweris rejected with409 Conflictwhen no other activeowneroradminwould remain. The response body is{"detail": "This would leave the deployment with no active Owner or Admin"}. Only a demotion out of that tier can trip this. Promoting someone, or moving betweenownerandadmin, never does.
Deactivate an admin: PATCH /api/v1/admin-users/{admin_user_id}/deactivate¶
Requires Owner or Admin. No request body.
Returns 200 OK with the admin record, now showing is_active: false.
There is no "reactivate" endpoint. If an account needs access restored,
that has to happen directly at the database level: this API only exposes
one-way deactivation.
The same two guardrail categories apply as for role changes:
- If the target account's role is
owner, the caller must be an Owner. Otherwise this returns403 Forbiddenwith{"detail": "Only an Owner can deactivate an Owner"}. - If deactivating this account would leave zero other active
owner-or-adminaccounts, it returns409 Conflictwith{"detail": "This would leave the deployment with no active Owner or Admin"}. Unlike the role-change guardrail, this check applies unconditionally here: there's no carve-out for a role that's stillowneroradmin, deactivation always removes the account from the active pool.
admin_user_id not matching any existing admin returns 404 Not Found.
Deactivating yourself¶
Nothing prevents an Owner or Admin from deactivating their own account. Only the "zero active Owner or Admin" and "Owner-only" guardrails above apply, and both are keyed on the target account, not on whether the target and the caller are the same admin. If you're the deployment's only Owner, deactivating yourself is blocked by the zero-Owner-or-Admin guardrail. If there's at least one other active Owner or Admin, it isn't blocked: this endpoint will let you lock yourself out of your own account.
Per-agent access¶
Only meaningful for editor or viewer accounts. owner and admin
accounts already see every agent unconditionally (see Roles
above).
Get an admin's granted agents: GET /api/v1/admin-users/{admin_user_id}/agent-permissions¶
Requires Owner or Admin.
Set an admin's granted agents: PUT /api/v1/admin-users/{admin_user_id}/agent-permissions¶
Requires Owner or Admin. Replaces the admin's entire granted-agent set with the list given. This isn't additive: omitting an agent id that was previously granted revokes it.
| Field | Type | Required | Description |
|---|---|---|---|
agent_ids |
array[integer] | Yes | The complete set of agent ids this admin should now see. |
Any id in agent_ids that doesn't match an existing agent returns
422 Unprocessable Entity. Granting an owner or admin account access
this way is harmless but pointless: they never consult it.
Every change here is audited¶
Invitations, role changes, and deactivations all write an
audit log entry (resource_type: "admin_user"). Setting
an admin's per-agent access writes one too
(resource_type: "agent_permissions"). Who changed whose access, and
when, is always reconstructable later.