# AGNT developer docs

> Every AGNT guide in one file, followed by the public endpoint index.

Base URL: `https://api.agnt.ai`

---

# Overview

Source: https://agnt.ai/reference/guides/overview

AGNT is infrastructure for AI personal assistants: the layers an agent runtime leaves out. Your app calls one API, and each of your users gets an assistant with its own name, phone number and email inbox. The assistant talks over iMessage, SMS, WhatsApp, email and voice, works in a cloud computer of its own, remembers what matters, and carries long-running work to completion. Every consequential action (email, messages, calls, calendar changes, file sharing, actions in connected apps) can wait for a person's approval, set per user and per action type.

Coding agents can read [`/llms.txt`](https://agnt.ai/llms.txt), an index of every guide, or [`/docs.md`](https://agnt.ai/docs.md), the full text of all guides plus an endpoint index in one file.

## Use cases

| You are building | What AGNT gives you |
|---|---|
| One branded assistant for consumers | Per-user isolation, an identity per assistant, messaging and voice, memory, durable tasks |
| An assistant feature inside an existing SaaS product | Users provisioned from your own sign-up, your brand on the assistant, approvals in your UI |
| A multi-tenant platform for businesses | Organizations and teams with their own admins, SSO and SCIM, autonomy policy per organization, audit history |

OpenAssistant ([openassistant.us](https://openassistant.us)) is a consumer assistant built entirely on AGNT.

## How it works

Your app creates users, assistants, chats and tasks through the API. Prime, the agent harness, does the work. Anything that leaves the platform goes out through an assistant's own identity or a user's connected account, subject to that user's autonomy settings. Your app never calls a model directly.

```text
 Your app (web, mobile, backend)
          |
          |  REST API  /  @agnt-sdk/client  /  agnt CLI
          v
 +-----------------------------------------------------------------+
 |  AGNT API  (https://api.agnt.ai)                                 |
 |    authentication, tenancy and per-user isolation on every call  |
 +-----------------------------------------------------------------+
          |
          v
 +-----------------------------------------------------------------+
 |  Prime, the agent harness                                        |
 |    runs every chat turn and every task, for every user           |
 +-----------------------------------------------------------------+
          |
          v
 +---------------------+---------------------+---------------------+
 | Identity and        | Voice               | Workspace           |
 | messaging           |   phone calls,      |   per-user cloud    |
 |   name, number,     |   live voice        |   computer: browser,|
 |   inbox, channels   |                     |   desktop, shell    |
 +---------------------+---------------------+---------------------+
 | Memory and context  | Tasks               | Tenancy and         |
 |   long-term memory, |   durable, async,   | approvals           |
 |   contacts,         |   long-running work |   orgs, teams,      |
 |   calendars         |                     |   autonomy, approval|
 +---------------------+---------------------+---------------------+
          |
          v
 The outside world
   phone networks (SMS, iMessage, WhatsApp, calls)
   email inboxes
   the web
   your users' connected Google and Microsoft accounts
```

Prime is model-agnostic: each model tier can be served by models from several providers, including open-weight models, and you change this without a code change. See [Prime: the harness](/reference/guides/prime).

## Core terms

| Term | Meaning |
|---|---|
| Account | Your app on AGNT. One account holds all of its end users, organizations and settings. |
| Organization | A customer company inside your account, with its own admins, domains, SSO and policy. Optional. |
| Team | A group of users inside an account or organization. Provisioning places each user on a team. |
| User | One of your end users. All user data (tasks, chats, memories, contacts, files) is isolated from every other user. |
| Assistant | An agent with its own name, email address and, optionally, phone number and voice. A user has a roster of assistants and one primary. |
| Chat | A conversation thread between a user and an assistant. |
| Task | A durable unit of work. It survives restarts, waits on people for days if needed, and resumes where it left off. |
| Run | One execution of a chat turn or a task by Prime. A task usually has many runs. |
| Approval | A consequential action waiting for a person to approve it. The API calls a batch of pending actions a cart. |
| Skill | A packaged capability Prime can use: instructions, knowledge, tools or an MCP server. |
| Workspace | The per-user cloud computer (browser, desktop, shell) Prime works in. |
| Autonomy settings | Per user, one approval mode for each action type: `auto`, `time-boxed` (runs after a review window) or `manual` (waits for approval). Admins can lock them. |
| Hosted app | The white-label end-user web app AGNT serves for your account on an AGNT subdomain or your custom domain. Users sign in, approve actions and take over their workspace there. Optional: the API supports the same flows in your own app. |
| Memory | A fact Prime keeps about a user and uses on every run. |
| Prime | AGNT's agent harness: the runtime that executes every chat turn and task. Invisible to your end users. |
| Model tier | `low`, `medium`, `high` or `ultra`. Work is routed by tier, never by a hard-coded model. |

## Account and user resources

| Kind | Examples | How to call |
|---|---|---|
| Account resources | Users, teams, organizations, assistants, skills, settings | An account API key, no user context |
| User resources | Tasks, chats, memories, contacts, calendars, approvals | An API key plus `X-User-Email: <user's email>` |

Isolation is between users. Each user resource belongs to exactly one user, and a user-scoped key acts only as its own user for approvals and edits. Work that acts for someone, such as creating a task for Jane Rivera, names the user with `X-User-Email: jane@harborview.co`. An email that matches no user in your account fails with `401` and never falls back to account-wide access.

An account key is your server's trusted credential. Without `X-User-Email` it can also list, read and delete tasks and chats across all of your users, for support and admin tooling. Keep account keys on your servers. See [What each credential can do](/reference/guides/authentication#what-each-credential-can-do).

## Platform facts

| Fact | Value |
|---|---|
| Base URL | `https://api.agnt.ai` |
| Auth | `Authorization: Bearer ak_live_...` (an API key) or a certificate JWT (the SDK). Add `X-User-Email` to act as one of your users. See [Authentication](/reference/guides/authentication). |
| Requests | JSON bodies, sent with `Content-Type: application/json` |
| Pagination | Most lists take `page` and `perPage` (default 20, max 100) and return `page`, `perPage` and `total`. Timelines, such as a task's activities, take `limit` and a `before` cursor and return `hasMore` and `cursor`. `GET /skills` takes `page` and `limit` (default 50, max 200). Call history and workspace snapshots take `limit`. SCIM uses `startIndex` and `count`. The [API reference](/reference/api) lists each endpoint's parameters. |
| Errors | `{ "status": 404, "error_code": "not_found", "error": "Task not found" }`. An invalid API key returns `401` with `{ "ok": false, "error": "Invalid or expired API key" }`. A rate-limited request returns `429` with `{ "ok": false, "error": "Too many requests", "retryAfter": ... }`. See [Errors](/reference/guides/authentication#errors). |
| Rate limits | Per account, client IP and path: 1,000 reads and 50 writes a minute by default. A limited request returns `429` with `Retry-After`. See [Rate limits](/reference/guides/authentication#rate-limits). |
| Versioning | None. Paths carry no version prefix. |
| Compliance | SOC 2 Type II, audited annually. Contact security@openassistant.us for the report. See [Security](/reference/guides/security). |
| Hosting | AWS, in the United States |
| Pricing | Usage is metered in credits, shown in the console. |

## Guides

| Guide | Covers |
|---|---|
| [Quickstart](/reference/guides/quickstart) | From an API key to a finished task |
| [Authentication](/reference/guides/authentication) | API keys, acting as a user, certificates, errors and rate limits |
| [Prime: the harness](/reference/guides/prime) | What Prime does, chats and tasks, memory and model tiers |
| [Assistants and messaging](/reference/guides/assistants-and-messaging) | Assistant identities, email, SMS, iMessage, WhatsApp and voice |
| [Computer use](/reference/guides/computer-use) | The per-user workspace: browser, desktop, shell and handoff to a person |
| [Tasks and approvals](/reference/guides/tasks-and-approvals) | Long-running work, autonomy, and approvals |
| [Users, organizations and teams](/reference/guides/organizations-and-teams) | Provisioning, organizations and teams, SSO, SCIM, audit export and admin policy |
| [Security](/reference/guides/security) | Isolation, encryption, identity, audit and reporting |
| [CLI and coding agents](/reference/guides/cli) | The `agnt` CLI, and building with Claude Code, Codex or Cursor |

The [API reference](/reference/api) documents every endpoint with request and response schemas.

## See also

- [Quickstart](/reference/guides/quickstart)
- [Authentication](/reference/guides/authentication)
- [Prime: the harness](/reference/guides/prime)

---

# Quickstart

Source: https://agnt.ai/reference/guides/quickstart

This guide takes you from an API key to a task your user's assistant is working on. Harborview, a property-management company, provisions its leasing manager Jane Rivera, gives her an assistant named Ava, and asks Ava to renew the Brightline Freight service contract. Every step is shown with curl, and the whole flow is repeated as one TypeScript script at the end.

## How it works

```text
 1. API key ----> 2. provision user ----> 3. finish user setup
                                                |
 6. poll status, read activity <---- 5. run <---- 4. assistant + task
```

Account calls (provision a user, manage assistants) send only `Authorization`. User calls (anything the user owns, such as her profile and tasks) also send `X-User-Email`. See [Authentication](/reference/guides/authentication).

## 1. Get an API key

In the [console](https://agnt.ai), open **Settings > API Keys** and generate a key. The key is shown once.

```bash
export AGNT_API_KEY=ak_live_...
export AGNT_USER_EMAIL=jane@harborview.co

curl https://api.agnt.ai/me \
  -H "Authorization: Bearer $AGNT_API_KEY"
```

```json
{ "ok": true, "accountId": "66f1a2b3c4d5e6f708192a3b", "email": null, "orgId": null, "userId": null, "scopes": [] }
```

`userId: null` confirms an account-wide key, which this guide uses. A `401` means the key is wrong or revoked.

## 2. Provision a user

`POST /users/provision` finds or creates a user by email. It is idempotent, so you can call it on every sign-in.

```bash
curl https://api.agnt.ai/users/provision \
  -X POST \
  -H "Authorization: Bearer $AGNT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "email": "jane@harborview.co", "firstName": "Jane", "lastName": "Rivera" }'
```

The response is `201` when the user was created and `200` when it already existed:

```json
{
  "ok": true,
  "user": {
    "id": "6702b1e8f9a0b1c2d3e4f5a6",
    "account": "66f1a2b3c4d5e6f708192a3b",
    "email": "jane@harborview.co",
    "firstName": "Jane",
    "lastName": "Rivera",
    "assistant": null,
    "timezone": "UTC",
    "status": "active"
  },
  "teamId": "6702ab52c3d4e5f6a7b8c9d0",
  "created": true,
  "addedToTeam": true,
  "alreadyMember": false,
  "joinSkipped": false
}
```

| Name | Type | Required | Description |
|---|---|---|---|
| `email` | string | Yes | The user's identity in your account. Lower-cased and trimmed. |
| `firstName`, `lastName` | string | No | Used in the assistant's introductions and signatures |
| `teamId` | string | No | Place the user on an existing team. Without it, the user gets a team of their own. |

```bash
export AGNT_USER_ID=6702b1e8f9a0b1c2d3e4f5a6
```

## 3. Finish the user's setup

Before an API key can act as a user, the user needs a timezone and working hours, confirmed once with `POST /onboarding/mark-onboarded`. Until then every user call except these three returns `428` with `error_code: "onboarding_required"`. If your own UI already runs these calls when the user signs up, skip this step.

```bash
curl https://api.agnt.ai/profile \
  -X PATCH \
  -H "Authorization: Bearer $AGNT_API_KEY" \
  -H "X-User-Email: jane@harborview.co" \
  -H "Content-Type: application/json" \
  -d '{ "timezone": "America/Los_Angeles" }'
```

Set working hours per day (`MO` to `SU`) in 24-hour local time:

```bash
curl https://api.agnt.ai/users/$AGNT_USER_ID/preferences/scheduling \
  -X PUT \
  -H "Authorization: Bearer $AGNT_API_KEY" \
  -H "X-User-Email: jane@harborview.co" \
  -H "Content-Type: application/json" \
  -d '{
    "schedulingPreferences": {
      "availability": {
        "workingHours": {
          "MO": [{ "start": "08:30", "end": "17:30" }],
          "TU": [{ "start": "08:30", "end": "17:30" }],
          "WE": [{ "start": "08:30", "end": "17:30" }],
          "TH": [{ "start": "08:30", "end": "17:30" }],
          "FR": [{ "start": "08:30", "end": "17:30" }]
        }
      }
    }
  }'
```

`PUT` replaces the scheduling preferences as a whole: any field you leave out returns to its default. Working hours are never blank: leave `workingHours` out and the saved hours are kept, and a `workingHours` with no slots on any day returns `400`. To change one field later, `GET` the same path and send the preferences back with your change.

Confirm setup:

```bash
curl https://api.agnt.ai/onboarding/mark-onboarded \
  -X POST \
  -H "Authorization: Bearer $AGNT_API_KEY" \
  -H "X-User-Email: jane@harborview.co"
```

```json
{ "ok": true }
```

## 4. Create an assistant

A user has a roster of assistants and one primary. Read the roster:

```bash
curl https://api.agnt.ai/users/$AGNT_USER_ID/assistants \
  -H "Authorization: Bearer $AGNT_API_KEY"
```

```json
{ "ok": true, "assistants": [], "primaryAssistantId": null }
```

If it is empty, create one. `POST /users/:userId/assistants` creates an assistant owned by this user and adds it to their roster. The first one becomes primary. The call is idempotent on `name`.

```bash
curl https://api.agnt.ai/users/$AGNT_USER_ID/assistants \
  -X POST \
  -H "Authorization: Bearer $AGNT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "name": "Ava" }'
```

```json
{
  "ok": true,
  "assistant": {
    "id": "6702b4c1d2e3f4a5b6c7d8e9",
    "name": "Ava",
    "email": "ava@assistants.harborview.co",
    "isPrimary": true
  }
}
```

The assistant gets its own email address on creation. To set its address, personality, voice or phone number, see [Assistants and messaging](/reference/guides/assistants-and-messaging).

```bash
export AGNT_ASSISTANT_ID=6702b4c1d2e3f4a5b6c7d8e9
```

## 5. Create and run a task

Tasks are user resources, so send `X-User-Email`. `title` and `assistant` (an assistant id on the user's roster) are required.

```bash
curl https://api.agnt.ai/tasks \
  -X POST \
  -H "Authorization: Bearer $AGNT_API_KEY" \
  -H "X-User-Email: jane@harborview.co" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Renew the Brightline Freight service contract",
    "description": "The current contract ends 2027-01-01. Ask Brightline (legal@brightlinefreight.com) for 2027 terms and keep the current rates if possible.",
    "assistant": "6702b4c1d2e3f4a5b6c7d8e9",
    "modelTier": "medium"
  }'
```

```json
{
  "ok": true,
  "task": {
    "id": "6702c3a9b8c7d6e5f4a3b2c1",
    "title": "Renew the Brightline Freight service contract",
    "status": "pending"
  }
}
```

| Name | Type | Required | Description |
|---|---|---|---|
| `title` | string | Yes | What the task should achieve |
| `assistant` | string | Yes | Id of an assistant on the user's roster. Any other id returns `403`. |
| `description` | string | No | The brief, in the user's words |
| `modelTier` | `low`, `medium`, `high` or `ultra` | No | Overrides the default tier for this task. See [Prime](/reference/guides/prime#model-tiers). |
| `onBehalfOf` | string | No | Email of the person the task acts for: the user, or someone who has delegated to them. Defaults to the user. |
| `followers` | string[] | No | Emails of people following the task |
| `skills` | string[] | No | Skills to make available to this task |

Creating a task does not start it. Start it with `POST /tasks/:taskId/process`. The optional `message` is added to the task as the user's words before the run.

```bash
export AGNT_TASK_ID=6702c3a9b8c7d6e5f4a3b2c1

curl https://api.agnt.ai/tasks/$AGNT_TASK_ID/process \
  -X POST \
  -H "Authorization: Bearer $AGNT_API_KEY" \
  -H "X-User-Email: jane@harborview.co" \
  -H "Content-Type: application/json" \
  -d '{ "message": "Attach Brightline-MSA-2026.pdf for reference." }'
```

```json
{
  "ok": true,
  "process": {
    "success": true,
    "taskId": "6702c3a9b8c7d6e5f4a3b2c1",
    "executionId": "6702c3b4a1b2c3d4e5f6a7b8",
    "message": "Task queued for processing"
  }
}
```

The call returns as soon as the run is queued. The work happens asynchronously and can take seconds or, for work that waits on other people, days.

## 6. Read status and activity

```bash
curl https://api.agnt.ai/tasks/$AGNT_TASK_ID \
  -H "Authorization: Bearer $AGNT_API_KEY" \
  -H "X-User-Email: jane@harborview.co"
```

The response wraps the task in `task`. These are the fields you read most:

| Name | Description |
|---|---|
| `status` | Where the task is in its lifecycle. See [Task statuses](/reference/guides/tasks-and-approvals). |
| `summary` | What the task has done, once there is something to report |
| `alarm` | When the task will wake itself to follow up (`wakeAt`), with a `headline` to show the user |
| `pendingCart` | Actions staged and waiting for approval, or `null` |
| `readyForReview` | `true` while the status is `awaiting_user_input` and the work is finished, waiting only for the user to look |
| `scheduledFor` | When a `scheduled` task will run |
| `completedAt` | When it finished |

Read the timeline, including the assistant's messages:

```bash
curl "https://api.agnt.ai/tasks/$AGNT_TASK_ID/activities?limit=50" \
  -H "Authorization: Bearer $AGNT_API_KEY" \
  -H "X-User-Email: jane@harborview.co"
```

```json
{
  "ok": true,
  "activities": [
    {
      "id": "6702c3d2e3f4a5b6c7d8e9f0",
      "type": "message",
      "createdAt": "2026-11-02T16:24:11.000Z",
      "message": {
        "role": "assistant",
        "content": "I drafted an email to legal@brightlinefreight.com asking for 2027 terms at current rates, with Brightline-MSA-2026.pdf attached. It is waiting for your approval."
      }
    }
  ],
  "hasMore": false,
  "cursor": null
}
```

`limit` defaults to 50 and is capped at 100. When `hasMore` is `true`, pass `cursor` as `before` to get the next, older page. `message.role` is `user`, `assistant` or `peer` (another assistant).

An email to Brightline is a consequential action, so depending on Jane's autonomy settings Ava stages it for approval and `pendingCart` is set. See [Tasks and approvals](/reference/guides/tasks-and-approvals) to approve it.

### Reply to a waiting task

When the status is `awaiting_user_input`, send the user's answer the same way you started the task:

```bash
curl https://api.agnt.ai/tasks/$AGNT_TASK_ID/process \
  -X POST \
  -H "Authorization: Bearer $AGNT_API_KEY" \
  -H "X-User-Email: jane@harborview.co" \
  -H "Content-Type: application/json" \
  -d '{ "message": "Ask for a two-year term as well." }'
```

If the task is mid-run, the message goes to the running task instead of starting a second run. On a `scheduled` task that has not run yet, `message` is required; it is attached and the task still runs at its scheduled time.

## The same flow in TypeScript

A complete script for Node.js 18 or later, using the built-in `fetch`. Save it as `quickstart.ts` and run it with `npx tsx quickstart.ts`.

```ts
const API = 'https://api.agnt.ai';
const KEY = process.env.AGNT_API_KEY!;
const EMAIL = 'jane@harborview.co';

type Json = Record<string, any>;

async function call(method: string, path: string, opts: { body?: Json; asUser?: boolean } = {}): Promise<Json> {
  const res = await fetch(`${API}${path}`, {
    method,
    headers: {
      Authorization: `Bearer ${KEY}`,
      'Content-Type': 'application/json',
      ...(opts.asUser ? { 'X-User-Email': EMAIL } : {}),
    },
    body: opts.body ? JSON.stringify(opts.body) : undefined,
  });
  const data = await res.json().catch(() => ({}));
  if (!res.ok) throw new Error(`${method} ${path} -> ${res.status} ${data.error_code ?? ''} ${data.error ?? ''}`);
  return data;
}

const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));

// Statuses in which a run is still in progress.
const RUNNING = new Set(['pending', 'executing', 'subtasks_running', 'paused']);

async function main() {
  // 2. Provision the user (idempotent)
  const { user } = await call('POST', '/users/provision', {
    body: { email: EMAIL, firstName: 'Jane', lastName: 'Rivera' },
  });

  // 3. Finish setup (safe to repeat)
  await call('PATCH', '/profile', { asUser: true, body: { timezone: 'America/Los_Angeles' } });
  const day = [{ start: '08:30', end: '17:30' }];
  await call('PUT', `/users/${user.id}/preferences/scheduling`, {
    asUser: true,
    body: { schedulingPreferences: { availability: { workingHours: { MO: day, TU: day, WE: day, TH: day, FR: day } } } },
  });
  await call('POST', '/onboarding/mark-onboarded', { asUser: true });

  // 4. Use the primary assistant, or create one
  const roster = await call('GET', `/users/${user.id}/assistants`);
  const assistantId: string =
    roster.primaryAssistantId ??
    (await call('POST', `/users/${user.id}/assistants`, { body: { name: 'Ava' } })).assistant.id;

  // 5. Create and run a task
  const { task } = await call('POST', '/tasks', {
    asUser: true,
    body: {
      title: 'Renew the Brightline Freight service contract',
      description: 'The current contract ends 2027-01-01. Ask Brightline (legal@brightlinefreight.com) for 2027 terms and keep the current rates if possible.',
      assistant: assistantId,
      modelTier: 'medium',
    },
  });
  await call('POST', `/tasks/${task.id}/process`, {
    asUser: true,
    body: { message: 'Attach Brightline-MSA-2026.pdf for reference.' },
  });

  // 6. Poll until the run settles (up to 5 minutes)
  let current: Json = task;
  for (let i = 0; i < 60 && RUNNING.has(current.status); i++) {
    await sleep(5000);
    current = (await call('GET', `/tasks/${task.id}`, { asUser: true })).task;
    console.log('status:', current.status, current.alarm?.headline ?? '');
  }

  // Print what the assistant said
  const { activities } = await call('GET', `/tasks/${task.id}/activities?limit=50`, { asUser: true });
  for (const a of activities) {
    if (a.message?.role === 'assistant' && a.message.content) console.log('\nAva:', a.message.content);
  }
}

main().catch((err) => { console.error(err); process.exit(1); });
```

Poll every few seconds while a run is in progress, and back off for tasks in a waiting status. Long waits are normal for tasks that depend on other people. To use `@agnt-sdk/client` instead of an API key, see [Certificate JWTs for the SDK](/reference/guides/authentication#certificate-jwts-for-the-sdk).

## Endpoints

| Method | Path | User call | Purpose |
|---|---|---|---|
| `GET` | `/me` | No | Inspect the key: account, organization, user, scopes |
| `POST` | `/users/provision` | No | Find or create a user by email |
| `PATCH` | `/profile` | Yes | Set the user's timezone, names and locale |
| `PUT` | `/users/:userId/preferences/scheduling` | Yes | Replace the user's scheduling preferences, including working hours |
| `POST` | `/onboarding/mark-onboarded` | Yes | Confirm setup so the key can act as the user |
| `GET` | `/users/:userId/assistants` | No | The user's roster and `primaryAssistantId` |
| `POST` | `/users/:userId/assistants` | No | Create an assistant for the user and add it to their roster |
| `POST` | `/tasks` | Yes | Create a task |
| `POST` | `/tasks/:taskId/process` | Yes | Start a task, or send the user's message to it |
| `GET` | `/tasks/:taskId` | Yes | Read a task |
| `GET` | `/tasks/:taskId/activities` | Yes | Read its timeline (`limit`, `before`) |

## See also

- [Authentication](/reference/guides/authentication): key levels, acting as a user, errors
- [Tasks and approvals](/reference/guides/tasks-and-approvals): task statuses, autonomy and approvals
- [Prime: the harness](/reference/guides/prime): what runs your tasks and chats

---

# Authentication

Source: https://agnt.ai/reference/guides/authentication

Every request to `https://api.agnt.ai` carries one credential in `Authorization: Bearer <credential>`. The credential decides which account the request belongs to and, for anything a user owns, which single user it acts as. That binding keeps each end user's data away from every other end user of your app.

## How it works

| Credential | Format | Use it for |
|---|---|---|
| API key | `ak_live_` followed by 64 hex characters | Server-to-server calls from your backend, curl and the `agnt` CLI. Covers the full public API. |
| Certificate JWT | An RS256 JWT with a `kid` header, signed with your private key | `@agnt-sdk/client` |

```text
 request --> credential --> account --> user (for user resources)
                 |
                 +-- account key ........ account-wide; add X-User-Email to act as a user
                 +-- organization key ... one organization, for its admin operations
                 +-- user key ........... exactly one user, always
```

Data a user owns (tasks, chats, memories, contacts, calendars, files, pending approvals) is isolated per user, not just per app. A user-scoped key acts only as its own user for approvals and edits, and an account key with `X-User-Email` acts as that user. An account key without the header is your server's trusted, account-wide credential: it cannot act as a user, but it can list, read and delete tasks and chats across all users.

## Create an API key

In the [console](https://agnt.ai), open **Settings > API Keys** and generate a key. The full key is shown once. AGNT stores only a hash of it, so a lost key cannot be recovered: revoke it and create another.

You can also create keys through the API:

```bash
curl https://api.agnt.ai/api-keys \
  -X POST \
  -H "Authorization: Bearer $AGNT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "label": "harborview-billing", "expiresAt": "2027-11-02T00:00:00.000Z" }'
```

```json
{
  "ok": true,
  "apiKey": {
    "id": "6702cb37e8f9a0b1c2d3e4f5",
    "account": "66f1a2b3c4d5e6f708192a3b",
    "organization": null,
    "user": null,
    "label": "harborview-billing",
    "prefix": "ak_live_3f9c2a1b",
    "scopes": [],
    "status": "active",
    "lastUsedAt": null,
    "expiresAt": "2027-11-02T00:00:00.000Z",
    "createdAt": "2026-11-02T16:24:11.000Z",
    "key": "ak_live_..."
  }
}
```

`key` appears in this response only. Afterwards the key is identified by `id` and `prefix`.

| Name | Type | Required | Description |
|---|---|---|---|
| `label` | string | Yes | A name for your records |
| `userId` | string | No | Create a user key for this user. Creating one for someone other than yourself requires an organization or account admin. |
| `expiresAt` | string | No | ISO 8601. The key stops authenticating at this time. |
| `scopes` | string[] | No | Strings stored on the key and returned on reads |

## Key levels

| Level | Created by | Reaches |
|---|---|---|
| Account | A developer on your account (console, or an account key) | The whole account: users, teams, organizations, assistants, skills. Acts as any user with `X-User-Email`. |
| Organization | An admin of one of your customer organizations, from their organization settings | Administrative operations for that organization |
| User | The user, or an admin on their behalf | Acts only as that user for approvals and edits |

The level is fixed when the key is created. Use the narrowest level that works: a backend job that serves one user holds a user key, not an account key.

## Act as a user

User resources need a user. With an account key, name the user in `X-User-Email`:

```bash
curl "https://api.agnt.ai/tasks?perPage=20" \
  -H "Authorization: Bearer $AGNT_API_KEY" \
  -H "X-User-Email: jane@harborview.co"
```

- The email must belong to a user in your account (any of the user's identifiers). Otherwise the request fails with `401`. It never falls back to account-wide access.
- Without the header, an account key reaches account resources. Calls that need a user, such as `POST /tasks`, fail.
- A user key always acts as its own user. Do not send `X-User-Email` with it.
- Before a key can act as a user for the first time, the user needs a timezone and working hours, confirmed with `POST /onboarding/mark-onboarded`. Until then user calls return `428 onboarding_required`. The [Quickstart](/reference/guides/quickstart#3-finish-the-users-setup) shows the three calls.

## Inspect a key

`GET /me` returns what the key resolves to:

```bash
curl https://api.agnt.ai/me \
  -H "Authorization: Bearer $AGNT_API_KEY"
```

```json
{ "ok": true, "accountId": "66f1a2b3c4d5e6f708192a3b", "email": null, "orgId": null, "userId": null, "scopes": [] }
```

An account key has `orgId` and `userId` set to `null`. An organization key has `orgId`. A user key has `userId` and `email`.

## Revoke and delete a key

`GET /api-keys` lists the keys you can manage, never the secret. Revocation takes effect immediately: the next request with the key fails with `401`.

```bash
curl -X POST https://api.agnt.ai/api-keys/6702cb37e8f9a0b1c2d3e4f5/revoke \
  -H "Authorization: Bearer $AGNT_API_KEY"
```

Only a revoked key can be deleted:

```bash
curl -X DELETE https://api.agnt.ai/api-keys/6702cb37e8f9a0b1c2d3e4f5 \
  -H "Authorization: Bearer $AGNT_API_KEY"
```

Revoking a key that is already revoked returns `400`. A key cannot be deleted while it is active, or while your hosted app uses it.

## Rotate a key

1. Create the new key.
2. Deploy it to every caller.
3. Revoke the old key. Its `lastUsedAt` in `GET /api-keys` shows whether anything still uses it.

## Certificate JWTs for the SDK

The Node SDK, `@agnt-sdk/client`, authenticates by signing short-lived JWTs with a private key that never leaves your servers. Register the matching public key in the AGNT console to get its key id (`kid`). AGNT holds only the public key.

Install the SDK (ESM, Node.js 18 or later):

```bash
npm install @agnt-sdk/client
```

Configure it in `agnt.config.js` at your project root. `AgntClient.create()` searches up from the working directory for this file.

```ts
export default {
  privateKey: process.env.AGNT_PRIVATE_KEY, // PKCS#8 PEM
  kid: process.env.AGNT_KID,
  apiUrl: 'https://api.agnt.ai',
};
```

`AgntClient.create()` returns a management client with an account-level token. `client.as(email)` returns a delegated client whose token carries that user's email. Calls through it currently act account-wide.

```ts
import { AgntClient, AgntApiError } from '@agnt-sdk/client';

const client = await AgntClient.create();
const jane = client.as('jane@harborview.co', { firstName: 'Jane', lastName: 'Rivera' });

try {
  const assistants = await client.assistants.list();
} catch (err) {
  if (err instanceof AgntApiError) console.error(err.status, err.errorCode, err.message);
  else throw err;
}
```

### Token contents

| Part | Value |
|---|---|
| Header | `{ "alg": "RS256", "kid": "<your kid>" }` |
| Claims | `iat`, `exp` (one hour after `iat`) |
| Delegated claims | `email`, plus `firstName` and `lastName` when passed to `client.as(email, { firstName, lastName })` |

The SDK caches each token per `kid` and email and signs a new one when less than five minutes remain.

- A certificate authenticates only its own account. The account comes from the registered certificate, never from claims in the token.
- Tokens with a `type` claim are refused.
- The certificate must be active. A revoked certificate fails with `401`.

## What each credential can do

| Operation | Account key | Account key + `X-User-Email` | User key |
|---|---|---|---|
| Provision users, manage rosters, teams and organizations | Yes | Not needed | No |
| Create and run tasks and chats; read and write memories | No: name a user | Yes, as that user | Yes, as its user |
| List, read and delete tasks and chats across all users | Yes (`GET /tasks`, `GET /tasks/:taskId`, `DELETE /tasks/:taskId`, `GET /chats`, `GET /chats/:chatId`, `DELETE /chats/:chatId`; narrow a list to one user with `ownerEmail` on tasks or `userEmail` on chats) | No, only that user's | No, only its user's |
| Approve, edit or cancel a pending action | No | Yes, relaying that user's decision | Yes |

These limits apply whatever the credential:

- **Approvals need a person.** Only the user a pending action belongs to can approve it: from their own signed-in session, or through your backend relaying their decision with an API key acting as them. A certificate token cannot approve, edit or cancel one (`403`), and neither can a console session acting on a user's behalf.
- **Some account settings are console-only.** Account-wide defaults such as the default model tiers (**Settings > Model**) are changed in the console by an account admin.
- **A blocked user is blocked everywhere.** When a user's status is `inactive` or `suspended`, every request acting as them fails with `403 account_blocked`.
- **A frozen account accepts no new work.** While the account-wide kill switch is engaged, creating, running or messaging tasks returns `423` with `error_code: "KILL_SWITCH_ENGAGED"`.

## Errors

API errors share one shape:

```json
{ "status": 404, "error_code": "not_found", "error": "Task not found" }
```

An invalid API key and a rate-limited request are refused before they reach an endpoint, with this shape:

```json
{ "ok": false, "error": "Too many requests", "retryAfter": 1793636711 }
```

| Status | `error_code` | When | What to do |
|---|---|---|---|
| `400` | `bad_request` (or a specific code) | Validation failed; `error` lists each field and problem | Fix the request |
| `401` | `unauthorized` | Missing or malformed credential; expired token; revoked or unknown certificate; `X-User-Email` that matches no user in the account | Check the credential and the email |
| `401` | none (`{ "ok": false }` body) | Unknown, revoked or expired API key | Use a valid key |
| `403` | `forbidden` | Authenticated but not allowed: wrong role, assistant not on the user's roster, approval from a certificate token or from an account key with no `X-User-Email` | Use a credential with the right level |
| `403` | `account_blocked` | The user is `inactive` or `suspended` | Unblock the user, or stop acting as them |
| `404` | `not_found` | The resource does not exist, or is not visible to this user | Check the id and the user |
| `409` | `conflict` (or a specific code) | Conflicts with current state, such as a concurrent change | Re-read, then retry |
| `423` | `KILL_SWITCH_ENGAGED` | The account is frozen by the kill switch | A person signed in to your hosted app releases the freeze |
| `428` | `onboarding_required` | The user has not finished setup | Set timezone and working hours, then `POST /onboarding/mark-onboarded` |
| `429` | none (`{ "ok": false }` body) | Rate limited | Wait `Retry-After` seconds, then retry |
| `500` | `server_error` | Unexpected failure | Retry with exponential backoff |

A resource that belongs to another user returns `404` rather than `403`, so its existence is not disclosed.

### Errors on every user call

Any call made as a user (an account key with `X-User-Email`, or a user key) can return these, whatever the endpoint. The API reference does not repeat them on each operation.

| Status | `error_code` | When |
|---|---|---|
| `401` | `unauthorized` | `X-User-Email` matches no user in the account |
| `403` | `account_blocked` | The user's `status` is `inactive` or `suspended` |
| `428` | `onboarding_required` | The user has not finished setup. Only the calls that finish it still work: `GET` and `PATCH /profile`, `GET` and `PUT /users/:userId/preferences/scheduling`, `POST /onboarding/mark-onboarded`, and listing, revoking and deleting API keys. |
| `423` | `KILL_SWITCH_ENGAGED` | Creating or running a task (`POST /tasks`, `POST /tasks/:taskId/process`) while the account is frozen |

An endpoint that needs a user, called with an account key and no `X-User-Email`, returns the error listed on that operation: usually `401 unauthorized`, or `403` or `404` where the call checks the task's owner or the caller's access.

## Rate limits

API-key traffic is rate limited per account, client IP and path: 1,000 requests a minute for reads (`GET`) and 50 a minute for writes by default. A limited request returns `429` with these headers. The body's `retryAfter` is the Unix time at which you may retry.

| Header | Description |
|---|---|
| `Retry-After` | Seconds until you may retry |
| `X-RateLimit-Limit` | The limit for this window |
| `X-RateLimit-Remaining` | `0` on a limited response |
| `X-RateLimit-Reset` | Unix time, in seconds, when the window resets |

Read the headers rather than hard-coding the numbers.

## Endpoints

| Method | Path | Purpose |
|---|---|---|
| `GET` | `/me` | What the API key resolves to |
| `GET` | `/api-keys` | List keys (never the secret) |
| `POST` | `/api-keys` | Create a key; the secret is returned once |
| `POST` | `/api-keys/:keyId/revoke` | Revoke immediately |
| `DELETE` | `/api-keys/:keyId` | Delete a revoked key |

## Best practices

- Send credentials only in the `Authorization` header, never in a URL or query string.
- Unknown fields in a request body are ignored. Check field names against the [API reference](/reference/api).
- Keep account keys on your servers. If your app calls AGNT from a browser or mobile client, route the call through your backend, which holds the key and sets `X-User-Email` from your own session, never from client input.

## See also

- [Quickstart](/reference/guides/quickstart): use a key end to end
- [Users, organizations and teams](/reference/guides/organizations-and-teams): provisioning, SSO and SCIM
- [Security](/reference/guides/security): isolation, encryption and audit

---

# Prime: the harness

Source: https://agnt.ai/reference/guides/prime

Prime is AGNT's agent harness: the runtime that executes every chat turn and every task, for every user, in every app built on AGNT. You do not write an agent loop, a tool router or a retry policy. You create chats and tasks, and Prime does the work. Your end users never see Prime: they talk to the assistant your app names and brands, such as Harborview's Ava.

## How it works

Each behavior below holds for every run, with no configuration.

| Behavior | Description |
|---|---|
| Decides how to answer | For each message, Prime decides whether a direct reply is enough or the request needs a durable task. A chat message that turns into real work opens a task that keeps going after the chat turn ends. |
| Plans and acts | Prime breaks work into steps and uses the assistant's tools and installed skills to do them: calendars, email, contacts, files, the web and the user's workspace. |
| Recovers | Failed steps are retried or worked around. A run that cannot finish leaves the task in a status that says why, such as `failed_access` or `credits_blocked`, and the task resumes once the cause is fixed. |
| Works in the user's workspace | Work that needs a browser, a desktop or a shell runs in that user's own workspace. See [Computer use](/reference/guides/computer-use). |
| Remembers | Prime reads the user's memories on every run and saves new facts as it learns them. Your app reads and writes the same memories. See [Memory](#memory). |
| Delegates | Large work is split across sub-agents running as subtasks. The parent task waits in `subtasks_running` and resumes with their results. |
| Follows autonomy settings | Every consequential outbound action (sending, booking, sharing, calling) goes through the user's autonomy settings. An approval executes exactly the content the person saw. See [Tasks, autonomy and approvals](/reference/guides/tasks-and-approvals#autonomy). |
| Stays inside one user | A run sees only the data of the user it runs for. Missing context never grants access. |
| Treats outside content as untrusted | Inbound email, web pages and documents are labelled untrusted to the model. Messages from unknown senders are gated, and unusual requests are held for the user. |
| Routes by tier | Every run uses a model tier, never a fixed model. See [Model tiers](#model-tiers). |

## Chats and tasks

Your app reaches Prime through two resources. A chat is a conversation: the user writes, the assistant answers. A task is a job with an outcome, such as "Renew the Brightline Freight service contract", that runs until it is done.

```text
 user message
      |
      v
 +--------------------+     quick answer     +----------------------+
 | Chat turn          |--------------------->| reply in the chat    |
 | (chat process      |                      +----------------------+
 |  call)             |     real work        +----------------------+
 +--------------------+--------------------->| Task (durable)       |
                                             |   runs, waits, wakes,|
 your backend -- POST /tasks --------------->|   asks for approval  |
              -- POST /tasks/:id/process --->|                      |
                                             +----------------------+
```

| | Chat | Task |
|---|---|---|
| Use it for | Conversation | Booking, research, coordination, follow-up |
| Lifetime | One turn per `POST /chats/:chatId/process` call | Many runs, from minutes to weeks, until done |
| Response | The turn's events as a `text/event-stream` | Asynchronous. Read `GET /tasks/:taskId` and its activities. |
| Created by | Your app | Your app, or Prime when a chat message needs one |

When Prime opens a task from a chat turn, the turn's `message` event includes the new `taskIds`. Both resources belong to a user: send `X-User-Email` with an account API key, or use a user key. See [Authentication](/reference/guides/authentication).

- [Assistants and messaging](/reference/guides/assistants-and-messaging) covers creating chats, sending messages and reading the event stream.
- [Tasks, autonomy and approvals](/reference/guides/tasks-and-approvals) covers creating, running and following tasks.
- The [Quickstart](/reference/guides/quickstart) runs a first task end to end.

## Memory

Memories are short facts Prime keeps about a user: preferences, important dates, people and standing instructions. Prime reads them on every run and writes new ones as it learns. Your app reads and writes the same store, for example to import what you already know about a user at sign-up.

### Create a memory

Save how Jane wants vendor quotes presented:

```bash
curl https://api.agnt.ai/memories \
  -X POST \
  -H "Authorization: Bearer $AGNT_API_KEY" \
  -H "X-User-Email: jane@harborview.co" \
  -H "Content-Type: application/json" \
  -d '{
    "content": "Jane prefers vendor quotes summarized in a table with price, lead time and warranty.",
    "tags": ["general"]
  }'
```

```json
{
  "ok": true,
  "memory": {
    "id": "6702c7f3a4b5c6d7e8f9a0b1",
    "account": "66f1a2b3c4d5e6f708192a3b",
    "user": "6702b1e8f9a0b1c2d3e4f5a6",
    "kind": "unstructured",
    "content": "Jane prefers vendor quotes summarized in a table with price, lead time and warranty.",
    "tags": ["general"],
    "source": "user",
    "scope": "user",
    "team": null,
    "startDate": null,
    "endDate": null,
    "isActive": true,
    "isExpired": false,
    "pinned": false,
    "metadata": {},
    "createdAt": "2026-11-02T16:24:11.000Z",
    "updatedAt": "2026-11-02T16:24:11.000Z"
  }
}
```

Tie a memory to a person with a `contact:` tag, and to a date with `date-memory`:

```bash
curl https://api.agnt.ai/memories \
  -X POST \
  -H "Authorization: Bearer $AGNT_API_KEY" \
  -H "X-User-Email: jane@harborview.co" \
  -H "Content-Type: application/json" \
  -d '{
    "content": "Always loop in Maria Chen on board meeting prep.",
    "tags": ["contact:maria@harborview.co"]
  }'

curl https://api.agnt.ai/memories \
  -X POST \
  -H "Authorization: Bearer $AGNT_API_KEY" \
  -H "X-User-Email: jane@harborview.co" \
  -H "Content-Type: application/json" \
  -d '{
    "content": "Dana Okafor at Brightline Freight handles contract renewals and wants redlines as PDF.",
    "tags": ["contact:legal@brightlinefreight.com"]
  }'

curl https://api.agnt.ai/memories \
  -X POST \
  -H "Authorization: Bearer $AGNT_API_KEY" \
  -H "X-User-Email: jane@harborview.co" \
  -H "Content-Type: application/json" \
  -d '{
    "content": "Board meeting week. Keep Jane'\''s calendar clear for Q1 2027 board meeting prep.",
    "tags": ["date-memory", "title:Board meeting week"],
    "startDate": "2026-11-16T00:00:00.000Z",
    "endDate": "2026-11-18T23:59:59.000Z"
  }'
```

The same calls in the TypeScript SDK:

```ts
const jane = client.as('jane@harborview.co');

await jane.memories.create({
  content: 'Always loop in Maria Chen on board meeting prep.',
  tags: ['contact:maria@harborview.co'],
});
```

The SDK also has `memories.list`, `get`, `update` and `delete`. See [Authentication](/reference/guides/authentication) for SDK credentials.

| Name | Type | Required | Description |
|---|---|---|---|
| `content` | string | Yes | The fact, in plain language. |
| `tags` | string[] | No | At least one tag from the vocabulary below. Defaults to `general`. Tags are trimmed, and `contact:` emails are lower-cased. |
| `scope` | string | No | `user` (default) or `team`. `team` shares the memory with a team the user is an active member of. |
| `team` | string | No | Team id, with `scope: "team"`. |
| `startDate` | string | No | ISO 8601. Required when `tags` includes `date-memory`. |
| `endDate` | string | No | ISO 8601. |
| `metadata` | object | No | Free-form data for your own use. |

### Tags

| Tag | Use |
|---|---|
| `general` | Anything not tied to one person, company or date. |
| `profile`, `profile:<slot>` | Facts about the user. `profile:bio` also carries `profile`. |
| `date-memory` | Time-bound facts. Set `startDate`, and `endDate` if it ends. |
| `assistant-style` | How the assistant should sound and write for this user. |
| `contact:<email>` | About one person. |
| `company:<companyId>` | About one company. |
| `event:<eventId>`, `task:<taskId>`, `file:<fileId>`, `workflow:<id>`, `skill:<name>`, `repo:<owner/repo>` | Tied to one calendar event, task, file, workflow, skill or code repository. |
| `title:<text>` | A display title for the memory, such as a name for a `date-memory`. |

A tag outside this vocabulary returns `400` with a message listing the accepted tags.

### List memories

List everything Jane's assistant knows about Maria:

```bash
curl "https://api.agnt.ai/memories?contactEmail=maria@harborview.co&perPage=50" \
  -H "Authorization: Bearer $AGNT_API_KEY" \
  -H "X-User-Email: jane@harborview.co"
```

| Name | Type | Required | Description |
|---|---|---|---|
| `tags` | string | No | Comma-separated. Matches any. |
| `contactEmail` | string | No | Same as `tags=contact:<email>`. |
| `companyId` | string | No | Same as `tags=company:<companyId>`. |
| `search` | string | No | Case-insensitive substring of `content`. |
| `scope` | string | No | `user` or `team`. |
| `page`, `perPage` | integer | No | `perPage` up to 100, default 20. |

Results are newest first, in `{ "ok": true, "memories": [], "page", "perPage", "total" }`.

### Visibility

A user sees their own memories and the team-scoped memories of teams they are an active member of. Nothing else crosses users. Only a memory's author can update or delete it. Notes Prime keeps for its own operation are not returned by the API.

## Model tiers

Every run uses one of four tiers: `low`, `medium`, `high` or `ultra`. Your code chooses tiers, never model ids. Which model serves each tier is set per account on the console's **Models** page (Model Strategies), so you change models without a code change. The console labels `medium` as Standard.

Prime is model-agnostic: each model tier can be served by models from several providers, including open-weight models, and you change this without a code change.

Each chat and task has its own `modelTier`, fixed when it is created. A chat or task your app creates through the API uses the first of these that is set:

1. The `modelTier` you pass to `POST /tasks` or `POST /chats`.
2. The organization's default, set with `PATCH /organizations/:orgId/settings/model-tiers`.
3. The account's default, set in the console under **Settings → Model**.
4. `medium`.

Chats and tasks that Prime creates on its own, for an inbound message or a task opened from a chat, check a matching segment's default first, then the organization, the account and `medium`. A segment is a named set of your organizations, teams or users, defined on the console's **Segments** page, with its own default tiers and model strategies. Use one to route a single customer to stronger models without changing anyone else's.

Chats and tasks have separate defaults. A tier value outside the four returns `400`. [Users, organizations and teams](/reference/guides/organizations-and-teams) shows how to set an organization's defaults.

## Extend Prime

- **Skills** package what Prime can use: instructions and knowledge, agents and prompts, CLI tools, plugins, workflows and MCP servers. Installed skills are available to the user's runs, and `POST /tasks` takes a `skills` list for one task. Manage skills with the [CLI](/reference/guides/cli) or in the console. Workflows, skills that start tasks on a schedule or an event, are covered in [Tasks, autonomy and approvals](/reference/guides/tasks-and-approvals#workflows).
- **MCP servers** are skills of kind `mcp`, created with `POST /skills`. `mcpServerUrl` is required. `mcpTransport` (`streamable-http` or `sse`) and `mcpAuthType` (`none`, `oauth` or `api_key`) are optional, and `name`, `title` and `description` come from the server when you omit them. Prime discovers the server's tools and calls them alongside its own.
- **Plugins** run sandboxed, with no access to platform secrets or other tenants' data.

MCP servers and the workspace are off under the restricted security preset. See [Security](/reference/guides/security).

Depend on the API's statuses, events and fields, not on the wording of the assistant's replies. Nothing an end user sees refers to Prime or AGNT unless you put it there.

## Endpoints

| Method | Path | Description |
|---|---|---|
| `POST` | `/chats/:chatId/process` | Run a chat turn. |
| `POST` | `/tasks` | Create a task. |
| `POST` | `/tasks/:taskId/process` | Start a task, or send it the user's message. |
| `GET` | `/memories` | List memories. |
| `POST` | `/memories` | Create a memory. |
| `GET` | `/memories/:memoryId` | Get a memory. |
| `PUT` | `/memories/:memoryId` | Update a memory. Every field is optional. |
| `DELETE` | `/memories/:memoryId` | Delete a memory. |
| `GET` | `/organizations/:orgId/settings/model-tiers` | Read an organization's default tiers. |
| `PATCH` | `/organizations/:orgId/settings/model-tiers` | Set an organization's default tiers. |
| `POST` | `/skills` | Create a skill, including kind `mcp` and kind `workflow`. |

## See also

- [Quickstart](/reference/guides/quickstart): a first task end to end
- [Tasks, autonomy and approvals](/reference/guides/tasks-and-approvals): durable work and human approval
- [Assistants and messaging](/reference/guides/assistants-and-messaging): assistants, channels and the chats API
- [Security](/reference/guides/security): isolation and untrusted content

---

# Assistants, messaging and calls

Source: https://agnt.ai/reference/guides/assistants-and-messaging

An assistant is the identity your users talk to: a name, a personality, a writing style, an email inbox and phone lines. Users reach it on the channels they already use. AGNT works out who is writing, opens or continues that person's chat, and Prime answers in the same thread. This guide follows Harborview as it gives Jane Rivera an assistant named Ava, connects Jane's phone, adds phone lines, and talks to Ava over the API, Slack and voice.

## How it works

```text
 Jane texts Ava's number (+14155550142) from +14155550187
        │
        ▼
 AGNT matches the sender ──► verified phone identifier → Jane Rivera
        │                    (no match → unknown-sender handling)
        ▼
 Jane's chat with Ava on iMessage ──► Prime runs a turn with Jane's memory,
        │                              tasks, connections and settings
        ▼
 Ava replies on the same channel, in the same thread
```

| Record | Description |
|---|---|
| Assistant | The identity: profile fields, an email address, phone lines (`channels`), a voice, and a `scope` that sets who can use it. |
| Identifier | A handle that belongs to one user: an email address, a phone number, or a sign-in account. A verified phone number or email address tells AGNT who sent an inbound message. |
| Chat | One conversation thread. Each 1:1 thread on a channel is its own chat. A user's tasks are shared across all of their chats. |

A phone thread belongs to the person, not the number. One assistant number can serve every user of your app, because AGNT tells users apart by the verified number they text from. When a number moves to a different user, the new holder does not inherit the previous holder's chat, history or tasks.

## Create an assistant

Create Ava on Jane's roster. The call is idempotent on `name` for that user, and the first assistant on an empty roster becomes the user's primary.

```bash
curl https://api.agnt.ai/users/6702b1e8f9a0b1c2d3e4f5a6/assistants \
  -X POST \
  -H "Authorization: Bearer $AGNT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Ava",
    "email": "ava@assistants.harborview.co",
    "personality": "Warm, brief and precise. Confirms details before acting.",
    "writingStyle": { "professionalism": 2, "humor": 1, "emojiUse": 0 }
  }'
```

```json
{
  "ok": true,
  "assistant": {
    "id": "6702b4c1d2e3f4a5b6c7d8e9",
    "account": "66f1a2b3c4d5e6f708192a3b",
    "organization": null,
    "team": null,
    "user": "6702b1e8f9a0b1c2d3e4f5a6",
    "scope": "user",
    "name": "Ava",
    "email": "ava@assistants.harborview.co",
    "avatarUrl": null,
    "signature": "--\nAssistant to {{name}}",
    "personality": "Warm, brief and precise. Confirms details before acting.",
    "writingStyle": { "professionalism": 2, "humor": 1, "emojiUse": 0 },
    "voice": null,
    "status": "active",
    "talkToStrangers": true,
    "inboxPlatform": null,
    "channels": {
      "imessage": null,
      "sms": null,
      "whatsapp": null,
      "voice": { "status": "none" }
    },
    "isPrimary": true,
    "createdAt": "2026-11-02T16:24:11.000Z",
    "updatedAt": "2026-11-02T16:24:11.000Z"
  }
}
```

The response is `201` for a new assistant and `200` when Ava already exists on Jane's roster.

| Name | Type | Required | Description |
|---|---|---|---|
| `name` | string | Yes | The assistant's name. |
| `email` | string | No | The assistant's own address, unique within your account. Generated when omitted. |
| `personality`, `backstory` | string | No | Free text that shapes how the assistant writes and talks. |
| `writingStyle` | object | No | `professionalism`, `humor` and `emojiUse`, each 0 to 3, plus optional free text `characterInspiration`, `externalCommunication` and `oneOnOneCommunication`. |
| `avatar` | string | No | Image URL. Returned as `avatarUrl`. |
| `signature`, `signatureEditorMode` | string | No | Email signature, and `visual` or `html`. |
| `gender`, `pronouns`, `country` | string | No | Persona details. `country` (ISO 3166-1 alpha-2) defaults to the user's country and never changes the output language. |
| `voice` | object | No | `{ voiceId, provider }` for calls. See [Live voice](#live-voice). |
| `metadata` | object | No | Your own key-value data. |

`channels` is read-only. Phone lines are added as described in [Phone lines](#phone-lines). `inboxPlatform` reports `google` or `outlook` when the assistant runs on a real Gmail or Outlook mailbox.

To check an address first, call `GET /assistants/check-email?slug=ava`, which returns `{ available, email }`. `GET /assistants/generate?name=Ava` suggests free addresses.

With the TypeScript SDK:

```ts
import { AgntClient } from '@agnt-sdk/client';

const client = await AgntClient.create();

const assistants = await client.assistants.list();
await client.assistants.update('6702b4c1d2e3f4a5b6c7d8e9', {
  personality: 'Warm, brief and precise.',
});
```

### Shared assistants

`POST /assistants` creates an assistant that several users share. Call it with an account-wide key and no `X-User-Email`.

| Name | Type | Required | Description |
|---|---|---|---|
| `name` | string | Yes | The assistant's name. |
| `email` | string | Yes | The assistant's address, unique within your account. |
| `scope` | string | No | `account` (default), `organization`, `team` or `user`. Sets who can see and hire the assistant. |
| `organization`, `team` | string | No | The organization or team, for those scopes. |
| `talkToStrangers` | boolean | No | Whether the assistant engages people who are neither users of your app nor known contacts. `true` by default. |

It also accepts every profile field in the table above.

## Rosters and the primary assistant

Every assistant a user can see is on their roster, and one roster member is their primary: the assistant that answers for that user by default. A user can have several assistants, each with its own identity and lines.

```bash
curl https://api.agnt.ai/users/6702b1e8f9a0b1c2d3e4f5a6/assistants/6702b4c1d2e3f4a5b6c7d8e9/primary \
  -X POST \
  -H "Authorization: Bearer $AGNT_API_KEY"
```

| Method | Path | Description |
|---|---|---|
| `GET` | `/users/:userId/assistants` | The roster, with `isPrimary` on each assistant and `primaryAssistantId`. |
| `POST` | `/users/:userId/assistants/:assistantId/hire` | Add an assistant the user can see to their roster. |
| `POST` | `/users/:userId/assistants/:assistantId/primary` | Make an assistant the primary. Returns the updated user. |
| `DELETE` | `/users/:userId/assistants/:assistantId` | Release one of the user's own assistants. |

Releasing an assistant deletes it and retires its email address. The call returns `409` for the primary, and for an assistant with open tasks unless you pass `?transfer=primary` to move that work to the primary. Shared and account-wide assistants cannot be released (`403`).

## Identifiers

AGNT matches inbound messages to users through identifiers. A phone number or secondary email resolves a sender only when it is verified. An unverified number is stored but identifies no one, so a text from it is handled as coming from an unknown sender.

Add Jane's mobile number as a verified identifier:

```bash
curl https://api.agnt.ai/identifiers \
  -X POST \
  -H "Authorization: Bearer $AGNT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "phone",
    "value": "+14155550187",
    "userId": "6702b1e8f9a0b1c2d3e4f5a6",
    "verified": true,
    "label": "mobile"
  }'
```

The response is `201` with the new `identifier` and the `user` it belongs to.

| Name | Type | Required | Description |
|---|---|---|---|
| `type` | string | Yes | `email`, `phone`, `google`, `microsoft`, `apple`, `external_id`, `employee_id` or `custom`. |
| `value` | string | Yes | The handle. Unique within your account. |
| `userId` | string | No | The user to add it to, with an account-wide key. Called as a user, it is added to that user. |
| `verified` | boolean | No | Whether the handle is verified. Honored only from account-wide credentials. |
| `label` | string | No | Your label, such as `mobile`. |

A verified phone number decides whose texts land in whose chat, and a verified email decides who signs in as whom. For that reason only account-wide credentials (the console or an account-wide API key) set `verified`. Set it only for numbers and addresses you have verified yourself. User-scoped callers verify through the hosted app's one-time-code flow, and a `verified` value they send is ignored.

Phone numbers are stored in E.164 form. A number without a leading `+` is parsed with the user's country as the default. An unparseable number returns `400` with `invalid_phone`. A value already held by any user in your account returns `400`.

Phone identifiers also report what AGNT has learned about the number: `iMessageCapable`, `whatsAppCapable`, `preferredTextPlatform`, and `messagingWindow` (when the person last messaged the assistant, per channel).

List Jane's phone numbers as Jane:

```bash
curl "https://api.agnt.ai/identifiers?type=phone" \
  -H "Authorization: Bearer $AGNT_API_KEY" \
  -H "X-User-Email: jane@harborview.co"
```

```ts
const jane = client.as('jane@harborview.co');
const phones = await jane.identifiers.list({ type: 'phone' });
```

| Type | Use |
|---|---|
| `email` | Sign-in and secondary addresses. `POST /identifiers/:identifierId/make-primary` makes one the user's primary email. |
| `phone` | Numbers the user texts and calls from. |
| `google`, `microsoft`, `apple` | Sign-in and connected accounts. |
| `external_id`, `employee_id`, `custom` | Your own keys for the user. |

`POST /identifiers/:identifierId/link` moves an identifier to another user and requires account-wide credentials. A user's last identifier and their primary email cannot be deleted.

## Channels

| Channel | 1:1 | Groups | Setup |
|---|---|---|---|
| iMessage | Yes | Yes | Assistant phone line |
| SMS | Yes | Yes | Assistant phone line |
| WhatsApp | Yes | No | Assistant phone line |
| Email (AGNT-hosted inbox) | Yes | Threads with several people | Automatic, at the assistant's `email` |
| Email (the user's Gmail or Outlook) | Yes | Threads with several people | The user connects over OAuth |
| Slack | DMs | Channels | Each assistant is its own Slack app |
| Microsoft Teams | Yes | No | An organization or team admin connects Teams |
| Phone calls | Inbound and outbound | No | Voice line on the assistant's number |
| Live voice | In your app | No | A voice engine on the assistant |

### Phone lines

Request numbers for the user's primary assistant with `POST /phone-line-requests`, called as the user.

```bash
curl https://api.agnt.ai/phone-line-requests \
  -X POST \
  -H "Authorization: Bearer $AGNT_API_KEY" \
  -H "X-User-Email: jane@harborview.co" \
  -H "Content-Type: application/json" \
  -d '{
    "requestedChannels": { "imessage": true, "sms": true, "voice": true },
    "notes": "415 area code"
  }'
```

```json
{
  "ok": true,
  "request": {
    "id": "6702cc48f9a0b1c2d3e4f5a6",
    "assistant": "6702b4c1d2e3f4a5b6c7d8e9",
    "requestedChannels": { "imessage": true, "sms": true, "whatsapp": false, "voice": true },
    "notes": "415 area code",
    "status": "pending",
    "createdAt": "2026-11-02T16:27:03.000Z"
  }
}
```

| Name | Type | Required | Description |
|---|---|---|---|
| `requestedChannels` | object | No | `imessage`, `sms`, `whatsapp` and `voice`, each a boolean that defaults to `false`. |
| `notes` | string | No | Preferences for the numbers, such as an area code. |

When the numbers are ready, the request's `status` becomes `fulfilled` and the numbers appear on the assistant under `channels.imessage`, `channels.sms` and `channels.whatsapp`. An assistant's voice line uses the same number as its iMessage line. `channels.voice.status` reports the voice line: `none`, `provisioning`, `pending_a2p` (waiting on US carrier registration), `active` or `failed`. A `dismissed` request carries a `dismissalReason`.

`GET /phone-line-requests/mine` returns the user's latest request, or `"request": null`. `POST /phone-line-requests` returns `400` when the user has no assistant or already has a pending request.

### iMessage, SMS and WhatsApp

Jane texts `+14155550142`. AGNT matches her verified number, continues her chat with Ava, and Prime replies on the same channel.

In iMessage and SMS group threads, the assistant works for its owner. Anything it does on the owner's behalf follows the owner's autonomy settings, and the owner's private conversations stay private.

WhatsApp allows a business to message a person freely only within 24 hours of that person's last message. AGNT tracks this per person in `messagingWindow`.

### Email

Every assistant has an AGNT-hosted inbox at its `email` address, and mail sent there reaches Prime like any other message. Inbound mail is dropped if it fails spam or virus checks, if DMARC fails for a domain that publishes `p=reject`, or if SPF and DKIM both fail. Prime treats inbound mail as untrusted content.

An assistant can run on a real Gmail or Outlook mailbox instead, which the user authorizes in the hosted app. `inboxPlatform` then reports `google` or `outlook`.

A user can also connect their own Gmail or Outlook over OAuth from the hosted app. Prime can then read, triage and draft in the user's mailbox and send as the user. Sending as the user is an outbound action and follows the user's autonomy settings.

### Slack

Each assistant can be its own Slack app, with its own bot name and avatar. `POST /assistants/:assistantId/slack-apps` creates the app and returns a manifest to apply in Slack. Give Ava a Slack app in Harborview's workspace, called as Jane:

```bash
curl https://api.agnt.ai/assistants/6702b4c1d2e3f4a5b6c7d8e9/slack-apps \
  -X POST \
  -H "Authorization: Bearer $AGNT_API_KEY" \
  -H "X-User-Email: jane@harborview.co" \
  -H "Content-Type: application/json" \
  -d '{ "kind": "workspace", "botDisplayName": "Ava" }'
```

The response is `201` with `slackApp` (`status` is `pending_credentials` until the app's Slack credentials are in place) and `manifest` (`manifestJson`, `createAppUrl`, `eventsUrl` and `redirectUrls`). Calling again for an existing app returns it with `200`.

| `kind` | The app | Call with | Errors |
|---|---|---|---|
| `marketplace` | Your listed app, for an account-wide assistant | An account key, no `X-User-Email` | `403` for any other caller. `400 not_system_assistant` for an assistant that is not account-wide. |
| `workspace` | An app internal to one Slack workspace, for a user's own or shared assistant | The user (`X-User-Email`), who must have the assistant and whose organization allows Slack | `403 user_required`, `403 slack_not_allowed`, `403 system_assistant_console_only` for an account-wide assistant. |

Users install the app with "Add to Slack" (`GET /oauth/slack/assistant/authorize`) and link their Slack identity with "Sign in with Slack" (`GET /oauth/slack/assistant/link`). `GET /assistants` and `GET /assistants/:assistantId` include a per-user `slack` status: `configured`, `connected`, and deep links to message the bot. Each workspace install has a kill switch and a setting for how the assistant treats Slack users who are not users of your app.

### Microsoft Teams

An organization or team admin connects Microsoft Teams with `GET /oauth/teams/authorize?target=team&targetId=6702ab52c3d4e5f6a7b8c9d0`, or `target=org` for the whole organization. Your account needs a Teams app configured first.

## Chats

A chat is one conversation thread between a user and an assistant. Messages on a channel land in that channel's chat automatically. Use the chats API to run an in-app conversation in your own product, or to read any of a user's threads. Every chats call is user-scoped.

### Start a chat and get a reply

1. Open a chat between Jane and Ava.

   ```bash
   curl https://api.agnt.ai/chats \
     -X POST \
     -H "Authorization: Bearer $AGNT_API_KEY" \
     -H "X-User-Email: jane@harborview.co" \
     -H "Content-Type: application/json" \
     -d '{ "assistantId": "6702b4c1d2e3f4a5b6c7d8e9", "title": "Brightline renewal" }'
   ```

   ```json
   {
     "ok": true,
     "chat": {
       "id": "6702c5d1e2f3a4b5c6d7e8f9",
       "title": "Brightline renewal",
       "status": "active",
       "platform": "chat",
       "user": "6702b1e8f9a0b1c2d3e4f5a6",
       "assistant": "6702b4c1d2e3f4a5b6c7d8e9",
       "messageCount": 0,
       "lastMessageAt": null,
       "createdForTaskId": null,
       "modelTier": "medium",
       "createdAt": "2026-11-02T16:30:12.000Z",
       "updatedAt": "2026-11-02T16:30:12.000Z"
     }
   }
   ```

2. Send Jane's message and run Prime on it.

   ```bash
   curl https://api.agnt.ai/chats/6702c5d1e2f3a4b5c6d7e8f9/process \
     -X POST \
     -H "Authorization: Bearer $AGNT_API_KEY" \
     -H "X-User-Email: jane@harborview.co" \
     -H "Content-Type: application/json" \
     -d '{ "message": { "content": "What is the status of the Brightline Freight renewal?" } }'
   ```

3. Read the thread.

   ```bash
   curl "https://api.agnt.ai/chats/6702c5d1e2f3a4b5c6d7e8f9/messages?perPage=20" \
     -H "Authorization: Bearer $AGNT_API_KEY" \
     -H "X-User-Email: jane@harborview.co"
   ```

`POST /chats` parameters:

| Name | Type | Required | Description |
|---|---|---|---|
| `assistantId` | string | Yes | One of the user's assistants. |
| `title` | string | No | The chat's title. |
| `metadata` | object | No | Your own key-value data. |
| `modelTier` | string | No | `low`, `medium`, `high` or `ultra`. Defaults to the organization's chat tier, then the account's, then `medium`. See [Prime](/reference/guides/prime). |

`POST /chats/:chatId/process` parameters:

| Name | Type | Required | Description |
|---|---|---|---|
| `message` | object | No | A user message to save and respond to: `content` (required), plus optional `files` (`[{ fileId }]` from the user's Files), `referenceId` and `metadata`. |
| `triggeringActivityId` | string | No | The id of an already-saved message to respond to. |

Omit the body to run on messages already saved. `POST /chats/:chatId/messages` saves a message without running Prime.

The response is a `text/event-stream`. The HTTP status is `200` even when the run ends in an error event.

| Event | Data |
|---|---|
| `status_update` | Progress during the run. |
| `interim_message` | `{ message, activityId }`, a message Prime sends before its final reply. |
| `message` | `{ ok, message }`, the reply. |
| `error` | `{ error, code, status }`. |

Each assistant message in `GET /chats/:chatId/messages` carries a read-only `delivery` object for outbound messages: `state` is `staged` (waiting in an approval), `held`, `provider_accepted` (sent and accepted for delivery) or `unknown` (no confirmed delivery record), with the `taskId` that sent it and the `cartId` of its approval, if any.

### List and manage chats

```bash
curl "https://api.agnt.ai/chats?platform=imessage,sms&perPage=20" \
  -H "Authorization: Bearer $AGNT_API_KEY" \
  -H "X-User-Email: jane@harborview.co"
```

| Name | Type | Description |
|---|---|---|
| `platform` | string | One channel or a comma-separated list: `chat`, `email`, `slack`, `teams`, `sms`, `whatsapp`, `imessage`, `phone`. |
| `status` | string | `active` (default) or `archived`. |
| `assistantId` | string | Only chats with this assistant. |
| `q` | string | Search. |
| `userEmail` | string | With an account key and no `X-User-Email`: only chats of this user. |
| `page`, `perPage` | integer | Pagination. `perPage` is at most 100. |

`GET /chats/:chatId` returns one chat, `DELETE /chats/:chatId` deletes it, and `GET /chats/:chatId/usage` returns tokens per run and the credits charged for each run.

## Outbound messages

Prime's replies to the user go out directly. Anything sent to someone else, such as an email to legal@brightlinefreight.com, a text, a Slack message or a call to a third party, is an outbound action and follows the user's autonomy settings. An approved action sends exactly the content that was shown. See [Tasks and approvals](/reference/guides/tasks-and-approvals).

People who are not users of your app are handled by the assistant's stranger settings: `talkToStrangers`, and the per-install setting on Slack. Unusual requests from them are held for the owner.

## Phone calls

A call to the assistant's number is answered with one of its greetings. When the caller is a recognized user, Prime runs the conversation with that user's memory and tasks. Prime places outbound calls when the user asks: to the user, or to a third party to get something done. Outbound calls follow the user's autonomy settings, with separate settings for calling as the assistant and calling as the user. Every attempt records its status, duration, a result summary and a transcript.

```bash
curl "https://api.agnt.ai/voice/calls?limit=25" \
  -H "Authorization: Bearer $AGNT_API_KEY" \
  -H "X-User-Email: jane@harborview.co"
```

```json
{
  "ok": true,
  "calls": [
    {
      "source": "direct",
      "id": "6702cd59a0b1c2d3e4f5a6b7",
      "relationship": "user",
      "target": { "phoneNumber": "+14155550187", "displayName": "Jane Rivera" },
      "goal": "Brief Jane on the Brightline Freight renewal before the board meeting",
      "status": "completed",
      "disposition": null,
      "startedAt": "2026-11-17T17:00:04.000Z",
      "endedAt": "2026-11-17T17:03:12.000Z",
      "durationSeconds": 188,
      "resultSummary": "Jane approved sending the 2027 terms to Brightline legal.",
      "transcript": "Ava: Hi Jane, it's Ava calling about the Brightline renewal."
    }
  ]
}
```

| Field | Description |
|---|---|
| `source` | `direct`, or `task` for an attempt Prime made on a task (with `taskId` and `taskTitle`). |
| `relationship` | `user`, `internal` (someone in the user's organization) or `third_party`. |
| `status` | Direct calls: `calling`, `completed`, `failed`, `no_answer`, `voicemail`. Task calls: `pending`, `preparing`, `calling`, `in_progress`, `completed`, `failed`, `needs_input`. |

`limit` is at most 100.

### Greetings

Each assistant has stored opening lines: `inbound` lines for answering, and `outbound` templates for calling someone by name. Each list holds 1 to 5 lines of up to 160 characters. Outbound templates contain exactly one `{firstName}`, and inbound lines contain none. Lines are generated from the assistant's personality, and lines you write are never overwritten by generation.

```bash
curl https://api.agnt.ai/assistants/6702b4c1d2e3f4a5b6c7d8e9/voice-greetings \
  -X PUT \
  -H "Authorization: Bearer $AGNT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "inbound": ["Hi, this is Ava with Harborview. How can I help?"],
    "outbound": ["Hi {firstName}, this is Ava from Harborview."]
  }'
```

`GET /assistants/:assistantId/voice-greetings` returns the lines, their `source` (`generated` or `owner`), `generating` while a generation runs, and the validation `limits`. `POST /assistants/:assistantId/voice-greetings/regenerate` takes `{ "scope": "both" | "inbound" | "outbound", "replaceOwnerEdits": false }`, up to 5 times per hour per assistant.

## Live voice

Users can talk to their assistant live in the hosted app and in your own apps. Each assistant uses its own engine and voice through `voice: { provider, voiceId }`. `GET /voice/providers` lists the engines your account has credentials for, each with its voices and default voice. `available` is `true` when an engine can take calls. Engines include live speech-to-speech engines that run on credentials you supply for your own cloud project. `GET /voice/providers` lists what is available. Voice models follow your account's model tier settings.

Start a session for Jane with Ava, scoped to the Brightline renewal task:

```bash
curl https://api.agnt.ai/voice/live/session \
  -X POST \
  -H "Authorization: Bearer $AGNT_API_KEY" \
  -H "X-User-Email: jane@harborview.co" \
  -H "Content-Type: application/json" \
  -d '{ "assistantId": "6702b4c1d2e3f4a5b6c7d8e9", "taskId": "6702c3a9b8c7d6e5f4a3b2c1" }'
```

```json
{
  "ok": true,
  "provider": "google-live",
  "sessionId": "3f6c2a9e-8b41-4d7a-9c15-2e7b0d4a6f18",
  "websocketUrl": "wss://voice.agnt.ai/live",
  "sessionToken": "lvs_9Qm2xT7rKc4bWz8pLf3nHd6vYa1sEj5u",
  "inputSampleRate": 16000,
  "outputSampleRate": 24000,
  "metadata": {
    "accountId": "66f1a2b3c4d5e6f708192a3b",
    "userId": "6702b1e8f9a0b1c2d3e4f5a6",
    "assistantId": "6702b4c1d2e3f4a5b6c7d8e9",
    "taskId": "6702c3a9b8c7d6e5f4a3b2c1"
  }
}
```

| Name | Type | Required | Description |
|---|---|---|---|
| `assistantId` | string | No | Defaults to the assistant of the chat or task. |
| `chatId` | string | No | Scope the session to a chat. |
| `taskId` | string | No | Scope the session to a task. With neither, a new chat is created. |

The client connects to `websocketUrl` with the single-use `sessionToken`. `POST /voice/live/cancel` with `{ sessionId }` cancels a session before it connects.

| Status | Code | Meaning |
|---|---|---|
| `404` | `call_context_not_found` | The assistant, chat or task is not this user's. |
| `409` | `voice_engine_not_configured`, `voice_not_configured` | The assistant's voice engine is not set up. |
| `429` | `voice_session_rate_limited` | Too many sessions in a short time. |

## Endpoints

| Method | Path | Description |
|---|---|---|
| `GET` | `/assistants` | Assistants visible to the caller, with per-user `slack` status |
| `POST` | `/assistants` | Create a shared assistant |
| `GET` `PUT` `DELETE` | `/assistants/:assistantId` | Read, update or delete an assistant |
| `GET` | `/assistants/check-email` | Whether an address is free (`?slug=`) |
| `GET` | `/assistants/generate` | Name and address suggestions (`?name=`) |
| `GET` `PUT` | `/assistants/:assistantId/voice-greetings` | Read or edit greeting lines |
| `POST` | `/assistants/:assistantId/voice-greetings/regenerate` | Regenerate greeting lines |
| `GET` `POST` | `/users/:userId/assistants` | Roster, or create an assistant on it |
| `POST` | `/users/:userId/assistants/:assistantId/hire` | Add a visible assistant to the roster |
| `POST` | `/users/:userId/assistants/:assistantId/primary` | Set the primary |
| `DELETE` | `/users/:userId/assistants/:assistantId` | Release an assistant |
| `GET` `POST` `PUT` `DELETE` | `/users/:userId/assistant` | The user's primary assistant |
| `GET` `POST` | `/identifiers` | List (`?type=&userId=`) or create identifiers |
| `GET` `PUT` `DELETE` | `/identifiers/:identifierId` | Read, update (`verified`, `label`, `metadata`) or delete |
| `POST` | `/identifiers/:identifierId/link` | Move to another user |
| `POST` | `/identifiers/:identifierId/make-primary` | Make an email the user's primary |
| `POST` | `/phone-line-requests` | Request phone lines |
| `GET` | `/phone-line-requests/mine` | The user's latest request |
| `POST` | `/assistants/:assistantId/slack-apps` | Create the assistant's Slack app |
| `GET` | `/oauth/slack/assistant/authorize` | "Add to Slack" URL |
| `GET` | `/oauth/slack/assistant/link` | "Sign in with Slack" URL |
| `GET` | `/slack/workspaces` | The user's linked Slack workspaces |
| `GET` | `/oauth/teams/authorize` | Connect Microsoft Teams (`?target=&targetId=`) |
| `GET` `POST` | `/chats` | List or create chats |
| `GET` `DELETE` | `/chats/:chatId` | Read or delete a chat |
| `GET` `POST` | `/chats/:chatId/messages` | List or save messages |
| `POST` | `/chats/:chatId/process` | Run Prime and stream the reply |
| `GET` | `/chats/:chatId/usage` | Tokens and credits per run |
| `GET` | `/voice/providers` | Voice engines and voices |
| `POST` | `/voice/live/session` | Start a live voice session |
| `POST` | `/voice/live/cancel` | Cancel a session before it connects |
| `GET` | `/voice/calls` | The user's call history |

## See also

- [Tasks and approvals](/reference/guides/tasks-and-approvals)
- [Prime](/reference/guides/prime)
- [Computer use](/reference/guides/computer-use)
- [API reference](/reference/api)

---

# Computer use

Source: https://agnt.ai/reference/guides/computer-use

Each user has a persistent cloud workspace: a Linux computer with Chrome, a desktop, a shell and a home directory. Prime uses it for work that has no API, such as filing a form on a government portal, pulling a report out of a vendor site, or writing and running code. When a site needs the user, for a sign-in or a one-time code, Prime opens a handoff, the user takes over the live browser, and the task continues once they hand it back. This guide follows Ava as she files the HVAC repair permit for Jane Rivera's task "Collect three vendor quotes for the lobby HVAC repair".

## How it works

```text
 Task needs a site with no API
        │
        ▼
 Prime opens a tab in Jane's workspace browser ──► browses, clicks, types, reads
        │
        ├── sign-in wall, MFA or payment page
        │        │
        │        ▼
        │   Handoff opens, the task pauses, Jane is asked to take over
        │        │
        │        ▼
        │   Jane drives the live browser, then hands back ──► the task resumes
        │
        ▼
 Files saved to Jane's Files, screenshots in the task feed,
 outbound actions through approvals
```

| Concept | Description |
|---|---|
| Workspace | One per user, shared by all of that user's tasks and assistants. Each user's workspace runs in its own container, and user files are kept in per-user directories. |
| Workspace browser | One Chrome profile per user. Each task works in its own tab. Sign-ins made here are kept for later tasks. |
| Handoff | A request from Prime for the user to take over the browser. The task pauses until the user hands back. |
| Session | The user's live, interactive view of the workspace during a handoff. |

### What Prime does in the workspace

- **Browses and acts on sites.** Prime navigates, reads pages, clicks, types, selects and scrolls, and posts screenshots to the task's activity feed so the user can follow progress.
- **Runs code.** Shell commands run with Python 3, Node.js, git, the GitHub CLI, curl and jq preinstalled. Each command runs for up to 60 seconds.
- **Produces files.** Documents, spreadsheets, presentations, PDFs, charts and data files. Finished files are saved to the user's Files, where they appear as deliverables on the task.
- **Delegates coding jobs.** Prime hands a coding task to Claude Code or Codex in the workspace. The coding agent clones the repository, creates a branch, implements and tests the change, and opens a pull request. Prime relays any question the coding agent asks mid-run. This requires the user's GitHub connection and a coding connection (an API key, or a Claude subscription for Claude Code).

Prime never asks for passwords in chat. When it reaches a sign-in, it opens a handoff.

### What persists

| Persists per user | Torn down |
|---|---|
| Files in the home directory | Running compute, after a period of inactivity |
| The browser profile: cookies and sign-ins | Running processes and shell sessions |
| Files saved to the user's Files | Caches cleared with `POST /workspace/cleanup` |
| Usage snapshots, for 30 days | |

The workspace starts again on the next task that needs it. The first start after an idle period takes longer. Keep anything a user should keep in their Files.

## Handoffs

Ava has the vendor quotes and now needs to file the repair permit on the county permit portal. The portal requires Jane's sign-in, so Ava opens a handoff and the task pauses. The hosted app runs this flow for Jane. To build it into your own app, use the steps below. Every workspace call is user-scoped: send `X-User-Email` with an account-wide key, or use a user-scoped token.

1. List what Prime is waiting on.

   ```bash
   curl https://api.agnt.ai/workspace/handoff/active \
     -H "Authorization: Bearer $AGNT_API_KEY" \
     -H "X-User-Email: jane@harborview.co"
   ```

   ```json
   {
     "ok": true,
     "handoffs": [
       {
         "id": "6702ca26d7e8f9a0b1c2d3e4",
         "taskId": "6702c4b0c9d8e7f6a5b4c3d2",
         "actionType": "login",
         "prompt": "Sign in to the county permit portal so I can file the HVAC repair permit for the lobby.",
         "targetUrl": "https://permits.harborcounty.gov/login",
         "status": "pending",
         "expiresAt": "2026-11-03T16:41:09.000Z",
         "createdAt": "2026-11-02T16:41:09.000Z"
       }
     ],
     "handoff": {
       "id": "6702ca26d7e8f9a0b1c2d3e4",
       "taskId": "6702c4b0c9d8e7f6a5b4c3d2",
       "actionType": "login",
       "status": "pending"
     }
   }
   ```

   `handoffs` lists every open handoff, oldest first. `handoff` is the newest on its own.

2. Start the live session and open `sessionUrl` for Jane.

   ```bash
   curl https://api.agnt.ai/workspace/handoff/6702ca26d7e8f9a0b1c2d3e4/session \
     -X POST \
     -H "Authorization: Bearer $AGNT_API_KEY" \
     -H "X-User-Email: jane@harborview.co"
   ```

   ```json
   {
     "ok": true,
     "sessionUrl": "https://workspace-proxy.agnt.ai/session/6702b1e8f9a0b1c2d3e4f5a6?token=...",
     "control": {
       "generation": 4,
       "handoffId": "6702ca26d7e8f9a0b1c2d3e4",
       "operator": "user"
     }
   }
   ```

   `sessionUrl` carries a single-use token. Open it once, for the user. If the page reloads, call `session` again for a fresh URL. While Jane has control, Prime cannot act in the browser.

3. Hand back when Jane has signed in.

   ```bash
   curl https://api.agnt.ai/workspace/handoff/6702ca26d7e8f9a0b1c2d3e4/complete \
     -X POST \
     -H "Authorization: Bearer $AGNT_API_KEY" \
     -H "X-User-Email: jane@harborview.co"
   ```

   ```json
   { "ok": true, "taskId": "6702c4b0c9d8e7f6a5b4c3d2" }
   ```

   Control returns to Ava, and the task resumes on the signed-in portal. The sign-in stays in Jane's browser profile for later tasks.

### Outcomes

| Outcome | Endpoint | Effect |
|---|---|---|
| Done | `POST /workspace/handoff/:handoffId/complete` | Closes the handoff and resumes the waiting task. Returns `{ taskId }`. |
| Later | `POST /workspace/handoff/:handoffId/defer` | Closes the handoff and gives the browser back to Prime, with a message and an optional time to try again. |
| No | `POST /workspace/handoff/:handoffId/decline` | Closes the handoff. Prime finds another way or reports that it cannot continue, and does not ask again for this task for a while. |

If Jane cannot sign in now, defer the handoff:

```bash
curl https://api.agnt.ai/workspace/handoff/6702ca26d7e8f9a0b1c2d3e4/defer \
  -X POST \
  -H "Authorization: Bearer $AGNT_API_KEY" \
  -H "X-User-Email: jane@harborview.co" \
  -H "Content-Type: application/json" \
  -d '{ "message": "After 5pm, I am on site until then.", "deferMinutes": 120 }'
```

| Name | Type | Required | Description |
|---|---|---|---|
| `message` | string | Yes | A message to Prime, such as when to come back. |
| `deferMinutes` | integer | No | Minutes from now to try again, up to 20160 (14 days). Without it, Prime decides when to ask again. |

The response is `{ "ok": true, "deferredUntil": "2026-11-02T18:43:00.000Z" }`.

Unanswered handoffs expire at `expiresAt`, and an idle session ends on its own. Handoff calls return `409` when control of the computer changed in the meantime. Refresh the handoff list and retry.

### Resolve several handoffs

`POST /workspace/handoff/resolve` ends the user's turn and reports an outcome for each handoff in one call.

```bash
curl https://api.agnt.ai/workspace/handoff/resolve \
  -X POST \
  -H "Authorization: Bearer $AGNT_API_KEY" \
  -H "X-User-Email: jane@harborview.co" \
  -H "Content-Type: application/json" \
  -d '{
    "items": [
      { "handoffId": "6702ca26d7e8f9a0b1c2d3e4", "outcome": "completed", "note": "Signed in to the permit portal." }
    ]
  }'
```

| Name | Type | Required | Description |
|---|---|---|---|
| `items` | array | Yes | 1 to 50 items. |
| `items[].handoffId` | string | Yes | The handoff. Ids that are not this user's are skipped. |
| `items[].outcome` | string | Yes | `completed`, `declined`, or `pending` to leave it open. |
| `items[].note` | string | No | A note to Prime, up to 2000 characters. |

The response lists the closed handoffs in `resolved`.

### Open the browser without a request

A user can open their workspace browser on their own, for example to sign in to the permit portal ahead of time. `POST /workspace/handoff/launch` returns `{ ok, handoffId, status: "starting" }`. Poll `GET /workspace/handoff/active` until that handoff is `ready`, then call `session`. Its `taskId` is `null`.

### Sign-in forms

When the hosted app identifies a sign-in form with confidence on a verified site, it can offer the user a form in place of the live browser. What the user types there is encrypted for the workspace and filled in directly. It is never visible to the assistant or to the AGNT API.

### Handoff fields

| Field | Values |
|---|---|
| `actionType` | `login`, `mfa`, `payment`, `form_fill`, `other` |
| `status` | `pending`, `starting`, `ready`, `active`, `completed`, `deferred`, `declined`, `closed`, `expired`, `failed` |
| `closeReason` | `completed`, `declined`, `deferred`, `expired`, `task_closed`, `superseded` |
| `urlSource` | `live` (read from the browser) or `claimed` (reported by the assistant when the browser could not be checked) |

## Watch a task's browser

`GET /workspace/watch` returns a still image of the tab Prime is using for one of the user's tasks. It does not take control and never starts the workspace. Poll it for a live view.

```bash
curl "https://api.agnt.ai/workspace/watch?taskId=6702c4b0c9d8e7f6a5b4c3d2" \
  -H "Authorization: Bearer $AGNT_API_KEY" \
  -H "X-User-Email: jane@harborview.co"
```

| Field | Description |
|---|---|
| `available` | `false` when there is nothing to show, with `reason: "watch_unavailable"`. |
| `pngBase64` | The frame, a base64 PNG. |
| `width`, `height` | Frame size in pixels. |
| `capturedAt` | When the frame was captured. |
| `sequence` | Increases with each new frame. |

## Usage and cleanup

`GET /workspace/profile` returns rolling usage, or `profile: null` until the workspace has run.

```bash
curl https://api.agnt.ai/workspace/profile \
  -H "Authorization: Bearer $AGNT_API_KEY" \
  -H "X-User-Email: jane@harborview.co"
```

```json
{
  "ok": true,
  "profile": {
    "lastActiveAt": "2026-11-02T16:52:30.000Z",
    "updatedAt": "2026-11-02T16:55:00.000Z",
    "peakMemoryBytes": 1879048192,
    "avgMemoryBytes": 1073741824,
    "memoryLimitBytes": 4294967296,
    "currentDiskBytes": 3221225472,
    "diskQuotaBytes": 21474836480,
    "avgCpuLoadAvg": 0.42,
    "totalActiveMinutes": 37,
    "topMcpSlugs": []
  }
}
```

`peakMemoryBytes` is the highest memory use in the last 24 hours. `GET /workspace/snapshots?limit=48` returns point-in-time usage, oldest first, captured at most every 5 minutes while the workspace runs and kept 30 days. `limit` is at most 288.

`POST /workspace/cleanup` frees space without touching the user's own files. It starts the workspace if it is not running.

```bash
curl https://api.agnt.ai/workspace/cleanup \
  -X POST \
  -H "Authorization: Bearer $AGNT_API_KEY" \
  -H "X-User-Email: jane@harborview.co" \
  -H "Content-Type: application/json" \
  -d '{ "scope": "cache" }'
```

| Name | Type | Required | Description |
|---|---|---|---|
| `scope` | string | No | `cache` (default) clears download caches and temporary files. `sites` also removes local working copies of the user's sites, which are restored when next needed. |

## Controls

Three switches govern computer use. Each is `"on"`, `"off"` or `null` (inherit), with a `*Locked` sibling that pins the value so a user's own preference is ignored.

| Switch | Covers |
|---|---|
| `workspaceCli` | The workspace shell: running commands, writing and deleting files, screenshots. |
| `browserAutomation` | Prime driving the workspace browser. |
| `workspaceHandoff` | Prime asking a user to take over the browser. |

`workspaceHandoffDomains` narrows where a handoff may go: `{ allowlist, blocklist }` of bare origins (`harborcounty.gov`, not URLs). An empty allowlist allows every origin not on the blocklist. Lists only tighten across tiers: allowlists intersect and blocklists add up. It has no lock.

Settings resolve from your account down to each team. An `"off"` at the account or at any of a user's teams wins. With no setting anywhere, each switch is on. Browser automation also requires the browser skill to be installed on your account. Account-wide settings are set in the AGNT console.

Limit the Maintenance team to handoffs on the county permit portal, and lock the shell off:

```bash
curl https://api.agnt.ai/teams/6702ab9fd4e5f6a7b8c9d0e1/settings/resource-permissions \
  -X PATCH \
  -H "Authorization: Bearer $AGNT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "capabilities": {
      "workspaceCli": "off",
      "workspaceCliLocked": true,
      "workspaceHandoffDomains": { "allowlist": ["harborcounty.gov"] }
    }
  }'
```

A team admin or an account-wide key can set team settings. A lock needs a value: `"workspaceCliLocked": true` with no `workspaceCli` returns `400`.

`POST /teams/:teamId/settings/security-preset` with `{ "preset": "restricted" }` turns off and locks MCP servers (including custom ones), the workspace shell, browser automation and workspace handoff for the team in one call. Without the shell and browser, Prime cannot act on services that have no API. Editing any switch by hand after applying the preset clears the preset label. See [Organizations and teams](/reference/guides/organizations-and-teams) for how team policy combines with the rest of your settings.

## Security

- **Per-user isolation.** Each user's workspace runs in its own container, and user files are kept in per-user directories. A session URL is valid only for the user it was issued to.
- **No public IP.** Workspaces accept no inbound connections from the internet. The live view reaches the user through an authenticated proxy using the single-use session token.
- **Unprivileged user.** Your commands, files and the browser run as an unprivileged user.
- **A locked-down browser.** Chrome's password manager, autofill, sync and browser sign-in are off. Prime can open only `http`, `https` and `data` URLs. Local files and cloud metadata endpoints are blocked.
- **Approvals apply.** Outbound actions Prime takes follow the user's autonomy settings, and page content is treated as untrusted. See [Tasks and approvals](/reference/guides/tasks-and-approvals).

See [Security](/reference/guides/security) for the platform-wide guarantees.

## Endpoints

| Method | Path | Description |
|---|---|---|
| `GET` | `/workspace/handoff/active` | Open handoffs for the user, oldest first |
| `POST` | `/workspace/handoff/launch` | Open the browser without a request from Prime |
| `POST` | `/workspace/handoff/:handoffId/session` | Live session URL |
| `POST` | `/workspace/handoff/:handoffId/complete` | Hand back and resume the task |
| `POST` | `/workspace/handoff/:handoffId/defer` | Ask Prime to come back later |
| `POST` | `/workspace/handoff/:handoffId/decline` | Refuse |
| `POST` | `/workspace/handoff/resolve` | Report outcomes for several handoffs |
| `GET` | `/workspace/watch` | Latest frame of a task's browser tab (`?taskId=`) |
| `GET` | `/workspace/profile` | Rolling usage |
| `GET` | `/workspace/snapshots` | Usage over time (`?limit=`) |
| `POST` | `/workspace/cleanup` | Clear caches and temporary files |
| `GET` `PATCH` | `/teams/:teamId/settings/resource-permissions` | Team switches and handoff domains |
| `POST` | `/teams/:teamId/settings/security-preset` | Apply the restricted preset |

## Limits

- One browser per user. Tasks share it, each in its own tab, and a handoff gives the user the whole browser until they hand back.
- Shell commands run for at most 60 seconds each. Longer work, such as coding jobs, runs in the background and Prime checks on it.

## See also

- [Tasks and approvals](/reference/guides/tasks-and-approvals)
- [Organizations and teams](/reference/guides/organizations-and-teams)
- [Security](/reference/guides/security)
- [API reference](/reference/api)

---

# Tasks, autonomy and approvals

Source: https://agnt.ai/reference/guides/tasks-and-approvals

A task is a durable unit of work an assistant owns until it is finished, such as "Renew the Brightline Freight service contract". A task survives restarts, waits days for a reply, and resumes on its own. Every action it takes in the outside world passes through the user's autonomy settings, which decide whether the action runs immediately, after a review window, or only after a person approves it.

## How it works

| Concept | Description |
|---|---|
| Task | The durable record: title, owner, assistant, status, plan and activity timeline. |
| Run | One execution of the task by [Prime](/reference/guides/prime). A task has many runs over its life. |
| Activity | One entry on the task's timeline: a message in or out, a status change, an approval. |
| Alarm | A scheduled wake. Every waiting task holds one, so no task is stranded. |
| Approval | A batch of staged actions waiting for a person. The API calls it a **cart**. |
| Autonomy settings | Per user, one approval mode for each action type. They decide which actions need approval. |

```text
  create ──> pending ──process──> executing ──┬──> completed
                                              │
                    ┌─────────────────────────┤ needs a person or a time
                    v                         │
   awaiting_user_input / awaiting_non_user_response / alarm_set / scheduled
                    │                         ^
                    └── reply arrives, ───────┘
                        alarm fires, or
                        user sends /process
```

Reads, research and drafting happen inside a run. Side effects (a message to someone other than the user, a calendar change, a shared file, a phone call) are staged in a cart and released according to the user's autonomy settings.

## Task statuses

| Status | Description |
|---|---|
| `pending` | Created or woken, waiting for a run to start. |
| `executing` | A run is in progress. |
| `awaiting_user_input` | Needs the user. Reply with `POST /tasks/:taskId/process`. |
| `awaiting_non_user_response` | Waiting on someone else, such as an email reply. `waitingOnContacts` names them. |
| `alarm_set` | Wakes itself at `alarm.wakeAt`. |
| `subtasks_running` | Waiting on its own subtasks. |
| `scheduled` | Starts at `scheduledFor`. |
| `on_hold` | Stopped by the user (`POST /tasks/:taskId/stop`) or held by the assistant. `POST /tasks/:taskId/process` resumes it. |
| `paused` | Held by the system. |
| `message_received`, `conversation` | A conversational thread at rest. A new message reopens it. |
| `deferred_access`, `failed_access` | Held, or stopped, for missing calendar or account access. |
| `credits_blocked` | Out of credits. Resumes when credits are restored. |
| `completed` | Finished by the assistant. |
| `done_user_marked` | Marked done by the user. |
| `handled_by_other` | Another assistant took the thread. `handledBy` says which. |
| `error`, `abandoned`, `archived` | Failed, given up, or archived. |

Every task has these guarantees:

- **No silent waits.** A task cannot enter `awaiting_user_input`, `awaiting_non_user_response` or `subtasks_running` without an active alarm. If nobody replies, the alarm wakes the task to follow up or report back.
- **Replies route home.** A reply from a person the task is waiting on, on the channel the assistant used to reach them, is attached to that task and wakes it. The next run sees the full history.
- **The owner decides when it is done.** Only the owner can stop a task or mark it done. Marking a task done discards any cart it has pending.

A task resumes when any of these happens:

1. A person it is waiting on replies.
2. Its alarm fires.
3. The user sends it a message with `POST /tasks/:taskId/process`.
4. A staged cart is approved, cancelled or executed. The next run is told the outcome.

## Create and run a task

All task calls act as a user. Pass `X-User-Email` with an account API key, or use a delegated SDK client. See [Authentication](/reference/guides/authentication).

Creating a task does not start it. Create the task, then call `process`.

```bash
curl https://api.agnt.ai/tasks \
  -X POST \
  -H "Authorization: Bearer $AGNT_API_KEY" \
  -H "X-User-Email: jane@harborview.co" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Renew the Brightline Freight service contract",
    "description": "Harborview'\''s MSA with Brightline Freight ends 2027-01-01. Get their 2027 terms.",
    "assistant": "6702b4c1d2e3f4a5b6c7d8e9",
    "modelTier": "medium"
  }'
```

```json
{
  "ok": true,
  "task": {
    "id": "6702c3a9b8c7d6e5f4a3b2c1",
    "type": "task",
    "title": "Renew the Brightline Freight service contract",
    "description": "Harborview's MSA with Brightline Freight ends 2027-01-01. Get their 2027 terms.",
    "status": "pending",
    "owner": "jane@harborview.co",
    "modelTier": "medium",
    "createdBy": "user",
    "pendingCart": null,
    "alarm": null,
    "createdAt": "2026-11-02T16:24:11.000Z",
    "updatedAt": "2026-11-02T16:24:11.000Z"
  }
}
```

| Name | Type | Required | Description |
|---|---|---|---|
| `title` | string | Yes | Short name for the task. |
| `assistant` | string | Yes | Id of an assistant the user has. Another assistant returns `403`. |
| `description` | string | No | What the user wants done, in their words. |
| `status` | string | No | `pending` (default) waits for `process`. `scheduled` starts on its own at `scheduledFor`. |
| `scheduledFor` | string | No | ISO 8601 time, with `status: "scheduled"`. |
| `modelTier` | string | No | `low`, `medium`, `high` or `ultra`. Fixed after the first run. See [Model tiers](/reference/guides/prime#model-tiers). |
| `skills` | string[] | No | Skill names the task should use. |
| `followers` | string[] | No | Email addresses of people following the task. |

Start the task. The same call sends the user's reply to a task in `awaiting_user_input`.

```bash
curl https://api.agnt.ai/tasks/6702c3a9b8c7d6e5f4a3b2c1/process \
  -X POST \
  -H "Authorization: Bearer $AGNT_API_KEY" \
  -H "X-User-Email: jane@harborview.co" \
  -H "Content-Type: application/json" \
  -d '{ "message": "Dana Okafor at legal@brightlinefreight.com handles renewals. The current MSA is attached.", "files": [{ "fileId": "6702c804b5c6d7e8f9a0b1c2" }] }'
```

```json
{
  "ok": true,
  "process": {
    "success": true,
    "taskId": "6702c3a9b8c7d6e5f4a3b2c1",
    "executionId": "6702c3e4a5b6c7d8e9f0a1b2"
  }
}
```

| Name | Type | Required | Description |
|---|---|---|---|
| `message` | string | No | A message from the user, saved to the task before the run. |
| `files` | object[] | No | Attachments already in the user's drive, as `{ "fileId" }`. Files the user cannot read are dropped. |

The same flow with the TypeScript SDK:

```ts
import { AgntClient } from '@agnt-sdk/client';

const client = await AgntClient.create();
const jane = client.as('jane@harborview.co');

const task = await jane.tasks.create({
  title: 'Renew the Brightline Freight service contract',
  assistant: '6702b4c1d2e3f4a5b6c7d8e9',
});
await jane.tasks.process(task.id, {
  message: 'Dana Okafor at legal@brightlinefreight.com handles renewals.',
});
```

## Follow a task

Read the task for its current state, and its activities for the timeline.

```bash
curl https://api.agnt.ai/tasks/6702c3a9b8c7d6e5f4a3b2c1 \
  -H "Authorization: Bearer $AGNT_API_KEY" \
  -H "X-User-Email: jane@harborview.co"

curl "https://api.agnt.ai/tasks/6702c3a9b8c7d6e5f4a3b2c1/activities?limit=50" \
  -H "Authorization: Bearer $AGNT_API_KEY" \
  -H "X-User-Email: jane@harborview.co"
```

Activities are newest first, in `{ "ok": true, "activities": [], "hasMore": true, "cursor": "<cursor>" }`. Page older entries with `before=<cursor>`, passing the cursor back unchanged. Each activity has a `type` and a `createdAt`, and a message activity has a `message` with `role`, `content` and `platform`.

| Field | Description |
|---|---|
| `status` | Where the task is in its lifecycle. |
| `summary` | What the assistant has done, once there is something to report. |
| `plan` | The assistant's checklist, each item `pending`, `done`, `skipped` or `blocked`. |
| `waitingOnContacts` | The people the task is waiting to hear from. |
| `alarm` | `wakeAt`, `headline` (the line to show the user) and `status`. |
| `pendingCart` | A summary of what is staged for approval, or `null`. |
| `effectiveApprovalMode`, `approvalLocks` | The autonomy mode that applies right now, and any admin locks. Returned by `GET /tasks/:taskId`. |

The [API reference](/reference/api/tasks) lists every field.

## Stop, finish and reopen a task

```bash
# Stop the current run. The task goes to on_hold.
curl -X POST https://api.agnt.ai/tasks/6702c3a9b8c7d6e5f4a3b2c1/stop \
  -H "Authorization: Bearer $AGNT_API_KEY" \
  -H "X-User-Email: jane@harborview.co"

# Mark done. Status becomes done_user_marked and any pending cart is discarded.
curl -X POST https://api.agnt.ai/tasks/6702c3a9b8c7d6e5f4a3b2c1/mark-done \
  -H "Authorization: Bearer $AGNT_API_KEY" \
  -H "X-User-Email: jane@harborview.co"

# Reopen a done task. The assistant wakes and asks how to proceed.
curl -X POST https://api.agnt.ai/tasks/6702c3a9b8c7d6e5f4a3b2c1/mark-not-done \
  -H "Authorization: Bearer $AGNT_API_KEY" \
  -H "X-User-Email: jane@harborview.co"

# Leave feedback: "like", "dislike", or null to clear. The value is accepted but not yet stored.
curl -X POST https://api.agnt.ai/tasks/6702c3a9b8c7d6e5f4a3b2c1/feedback \
  -H "Authorization: Bearer $AGNT_API_KEY" \
  -H "X-User-Email: jane@harborview.co" \
  -H "Content-Type: application/json" \
  -d '{ "status": "like" }'
```

```ts
await jane.tasks.stop(task.id);
await jane.tasks.feedback(task.id, 'like');
```

## Schedule a task

Create a task with `"status": "scheduled"` and a future `scheduledFor`. It starts on its own at that time.

```bash
curl https://api.agnt.ai/tasks \
  -X POST \
  -H "Authorization: Bearer $AGNT_API_KEY" \
  -H "X-User-Email: jane@harborview.co" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Collect three vendor quotes for the lobby HVAC repair",
    "assistant": "6702b4c1d2e3f4a5b6c7d8e9",
    "status": "scheduled",
    "scheduledFor": "2026-11-09T16:00:00.000Z"
  }'
```

Follow-ups inside a task need no scheduling from your app. The assistant sets the task's `alarm` when it needs to check back, and `alarm.headline` is the line to show the user.

## Workflows

Recurring and event-driven work is a workflow: a skill with `kind: "workflow"` and one or more `triggers`. Each firing creates a task.

| Trigger `on` | Fires |
|---|---|
| `cron` | On a fixed schedule. `schedule.frequency` is `minutely`, `hourly`, `daily`, `weekly` or `monthly`. |
| `webhook` | On a calendar or email event, such as `event.scheduled`, `event.cancelled` or `email.received`, immediately or after `delayOffset`. |
| `timestamp` | Relative to a time on the matched event, such as 30 minutes before `startTime`. |
| `condition` | When a polled condition becomes true. |
| `inactivity` | When an expected event did not happen within a window. |
| `system` | When another workflow's task completes or fails. |
| `compound` | When several sub-triggers combine with `and` or `or`. |
| `manual` | When the user runs it. |

Create a Monday-morning portfolio briefing for Jane:

```bash
curl https://api.agnt.ai/skills \
  -X POST \
  -H "Authorization: Bearer $AGNT_API_KEY" \
  -H "X-User-Email: jane@harborview.co" \
  -H "Content-Type: application/json" \
  -d '{
    "kind": "workflow",
    "name": "weekly-portfolio-briefing",
    "title": "Weekly portfolio briefing",
    "description": "A Monday-morning summary of leases, maintenance and vendor contracts across the portfolio",
    "instructions": "Summarize leases expiring in the next 60 days, open maintenance tickets, and vendor contracts up for renewal.",
    "triggers": [{
      "on": "cron",
      "schedule": { "frequency": "weekly", "days": ["MO"], "hour": 8, "minute": 0 },
      "timezone": "America/Los_Angeles"
    }],
    "maxRuns": 52
  }'
```

| Name | Type | Required | Description |
|---|---|---|---|
| `kind` | string | Yes | `workflow`. |
| `name` | string | Yes | Unique slug. |
| `title` | string | Yes | Display name. |
| `description` | string | Yes | What the workflow does. |
| `instructions` | string | No | What each run does. |
| `triggers` | object[] | Yes | When the workflow fires. |
| `scheduleType` | string | No | `now`, `recurring` or `trigger-based`. Inferred from `triggers` when omitted. |
| `maxRuns`, `recurrenceEndsAt` | integer, string | No | Stop after this many runs, or at this time. |
| `maxConcurrent`, `cooldownMs` | integer | No | Limit overlapping runs and how often the workflow can fire. |
| `overflowBehavior` | string | No | `queue`, `drop` or `replace`, when a firing arrives at the concurrency limit. |

With `X-User-Email`, the workflow belongs to that user. With an account key and no `X-User-Email`, the workflow is available app-wide. Manage skills of every kind with the [CLI](/reference/guides/cli).

## Autonomy

Each user has a supervision matrix (`supervisionMatrix`): one cell per action type, each holding a mode.

| Mode | Behavior |
|---|---|
| `auto` | Executes immediately. |
| `time-boxed` | Waits `timeBoxedWindowMin` minutes so the user can review, edit or cancel. When the window ends the cart executes, or is cancelled if `timeBoxedExpiry` is `cancel`. While it waits, the cart's status is `time_boxed`. |
| `manual` | Waits until a person approves or cancels. |

A cell can also set `undoDelaySec`: how many seconds an action that cannot be reversed (a sent message, a call) waits after it is released, before it goes out. Undo during that hold cancels it. Unset, the hold is 30 seconds. `0` sends at once.

| Action type | Covers | New-user default |
|---|---|---|
| `email_as_user` | Email sent as the user, from their mailbox | `manual` |
| `email_as_assistant` | Email from the assistant's own address | `auto` |
| `messaging` | SMS, iMessage, WhatsApp and chat messages | `auto` |
| `voice_call_as_assistant` | Calls where the assistant speaks as itself | `auto` |
| `voice_call_as_user` | Calls where the assistant speaks for the user | `manual` |
| `calendar` | Creating, changing and cancelling events | `auto` |
| `drive` | Creating, writing and sharing files in a connected drive | `auto` |
| `files` | Files in the assistant's own storage | `auto` |
| `contacts` | Changes to the user's contacts | `auto` |
| `third_party_skills` | Actions through connected apps and MCP servers | `auto` |
| `website` | Publishing a site | `manual` |

Reads are never gated. The defaults favor getting work done: only actions taken as the user, and publishing a site, wait for approval. For enterprise use, set `email_as_assistant`, `messaging`, `voice_call_as_assistant`, `calendar`, `drive` and `third_party_skills` to `manual` or `time-boxed`, and lock them at the organization so users cannot loosen them.

### Read and change a user's settings

```bash
curl https://api.agnt.ai/users/6702b1e8f9a0b1c2d3e4f5a6/preferences/supervision/matrix \
  -H "Authorization: Bearer $AGNT_API_KEY"
```

```json
{
  "ok": true,
  "supervisionMatrix": {
    "matrix": {
      "email_as_user": { "mode": "manual" },
      "email_as_assistant": { "mode": "auto" },
      "messaging": { "mode": "auto" }
    },
    "matrixUpdatedAt": "2026-11-02T16:10:03.000Z",
    "parentLockedCells": ["email_as_user"],
    "orgLockedCells": ["email_as_user"],
    "teamLockedCells": [],
    "lockedValues": { "email_as_user": { "mode": "manual" } }
  }
}
```

`PATCH` merges per cell:

```bash
curl https://api.agnt.ai/users/6702b1e8f9a0b1c2d3e4f5a6/preferences/supervision/matrix \
  -X PATCH \
  -H "Authorization: Bearer $AGNT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "matrix": {
        "email_as_assistant": { "mode": "manual" },
        "messaging": { "mode": "time-boxed", "timeBoxedWindowMin": 5 }
      } }'
```

| Name | Type | Required | Description |
|---|---|---|---|
| `matrix.<actionType>.mode` | string | No | `auto`, `time-boxed`, `manual`, or `null` to inherit. |
| `matrix.<actionType>.timeBoxedWindowMin` | number | No | Window length in minutes, for `time-boxed`. |
| `matrix.<actionType>.timeBoxedExpiry` | string | No | `execute` or `cancel`, when the window ends. |
| `matrix.<actionType>.undoDelaySec` | number | No | Hold before an irreversible action goes out. |

A `PATCH` that would loosen a cell locked by a team or the account returns `403`. `PUT /users/:userId/preferences/supervision/matrix/reset` clears every one of the user's own cells and removes every recipient exception. Each action type then follows the team or organization value if there is one, else the platform default: `manual` for `email_as_user`, `voice_call_as_user`, `third_party_skills` and `website`, and `auto` for the rest.

### Recipient exceptions

An exception changes the mode for specific people or apps. Require approval for anything Ava sends to Brightline Freight:

```bash
curl https://api.agnt.ai/users/6702b1e8f9a0b1c2d3e4f5a6/preferences/supervision/exceptions \
  -X POST \
  -H "Authorization: Bearer $AGNT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "label": "Brightline Freight",
    "match": { "kind": "domain", "value": "brightlinefreight.com" },
    "mode": "manual",
    "appliesTo": ["email_as_assistant", "email_as_user", "messaging"]
  }'
```

```json
{
  "ok": true,
  "recipientException": {
    "id": "6702cc48f9a0b1c2d3e4f5a6",
    "label": "Brightline Freight",
    "match": { "kind": "domain", "value": "brightlinefreight.com" },
    "mode": "manual",
    "timeBoxedWindowMin": null,
    "timeBoxedExpiry": null,
    "undoDelaySec": null,
    "appliesTo": ["email_as_assistant", "email_as_user", "messaging"],
    "createdAt": "2026-11-02T16:12:40.000Z"
  }
}
```

| Name | Type | Required | Description |
|---|---|---|---|
| `match.kind` | string | Yes | `email`, `domain`, `contactId`, `company`, `tag`, `priority` (`"1"` to `"3"`), `internal` (`"true"` or `"false"`) or `connection` (a connected app, by skill name). |
| `match.value` | string | Yes | The value to match. |
| `mode` | string | Yes | `auto`, `time-boxed`, `manual`, or `denied` to block the action. |
| `appliesTo` | string[] | No | Action types it covers. Empty, or `["All"]`, covers every action type with a recipient. |
| `label` | string | No | Up to 60 characters. |
| `timeBoxedWindowMin` | integer | No | 1 to 60. |
| `undoDelaySec` | integer | No | 0 to 300. |

A loosening exception applies only when every recipient of the action is covered by one. Each user can have up to 100 exceptions. Remove one with `DELETE /users/:userId/preferences/supervision/exceptions/:exceptionId`.

### Per-task override

A person can set the mode for a single task, for every kind of action on it:

```bash
curl https://api.agnt.ai/tasks/6702c3a9b8c7d6e5f4a3b2c1 \
  -X PUT \
  -H "Authorization: Bearer $AGNT_API_KEY" \
  -H "X-User-Email: jane@harborview.co" \
  -H "Content-Type: application/json" \
  -d '{ "approvalOverride": { "mode": "manual" } }'
```

Set `approvalOverride` to `null` to inherit again. The assistant can tighten a task's mode on its own, never loosen it. An admin lock outranks the override.

### Organization and team policy

Admins set autonomy cells for an organization or team and can lock them. A locked cell is pinned to the admin's value: the user's own setting, exceptions and task overrides cannot change it. An unlocked admin value is only a default, which the user's own cell replaces.

```bash
curl https://api.agnt.ai/organizations/6702a9c3e1b4f5d6a7b8c901/settings/resource-permissions \
  -X PATCH \
  -H "Authorization: Bearer $AGNT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "matrix": {
        "email_as_user": { "mode": "manual" },
        "email_as_assistant": { "mode": "manual" },
        "locked": ["email_as_user", "email_as_assistant"]
      } }'
```

For a team, send `{ "approvalSettings": { "matrix": { } } }` with the same cells to `PATCH /teams/:teamId/settings/resource-permissions`. Team admins can set and lock only cells the organization has not locked. Where several locks apply, the most conservative wins. [Users, organizations and teams](/reference/guides/organizations-and-teams) covers the rest of organization and team policy.

The mode for an action is decided in this order:

1. A `denied` exception blocks the action.
2. An admin lock decides.
3. The task's `approvalOverride`.
4. The most conservative matching exception.
5. The user's own cell.
6. The platform default.

The task reports the result in `effectiveApprovalMode` and any admin locks in `approvalLocks`.

## Approvals

When an action needs review, the task stages it in a cart and waits. A cart's id is the id of the task that staged it. The approval endpoints live under `/portal` on `api.agnt.ai` and take the same API key and `X-User-Email` as every other call.

| Cart status | Description |
|---|---|
| `open` | The run is still staging actions. Nothing is submitted for review. |
| `submitted` | Staging finished and the approval mode is being resolved. Moves on within moments. |
| `pending_approval` | Waiting for a person (`manual`). |
| `time_boxed` | Executes at `timeBoxedExpiresAt` unless edited or cancelled. |
| `approved` | A person approved it, or its window ended. Execution is about to start. |
| `executing` | The stored actions are being sent. |
| `executed` | Every action ran. |
| `cancelled` | Cancelled with `POST /portal/carts/:cartId/cancel`, or its window ended with `timeBoxedExpiry: "cancel"`. Nothing was sent. |
| `rejected` | Declined from an approval link, such as the Reject link in an approval email. Nothing was sent. |
| `failed` | Execution failed. The task's activities show which actions, if any, went out. |

Every approval has these guarantees:

- **Exactly what was shown.** Every cart response carries a `contentHash` of its staged content. Approve must echo it. If the content changed since it was shown, approve returns `409 cart_content_changed` and nothing is sent.
- **No model in between.** Approval executes the stored actions. No model call runs between approval and execution, so nothing is rewritten after review.
- **A person approves.** Approve, edit, cancel and undo accept a person's own session or an API key acting as that person. Certificate JWTs, the SDK's credential, get `403`, so the SDK has no approval methods.
- **Edits ship verbatim.** Editing a staged email's subject or body changes the content hash, and the edited text is what goes out.

### Review and approve

List the carts waiting for the user, then read one for its staged actions and `contentHash`:

```bash
curl https://api.agnt.ai/portal/carts \
  -H "Authorization: Bearer $AGNT_API_KEY" \
  -H "X-User-Email: jane@harborview.co"

curl https://api.agnt.ai/portal/carts/6702c3a9b8c7d6e5f4a3b2c1 \
  -H "Authorization: Bearer $AGNT_API_KEY" \
  -H "X-User-Email: jane@harborview.co"
```

```json
{
  "ok": true,
  "cart": {
    "id": "6702c3a9b8c7d6e5f4a3b2c1",
    "taskId": "6702c3a9b8c7d6e5f4a3b2c1",
    "taskTitle": "Renew the Brightline Freight service contract",
    "assistantName": "Ava",
    "status": "pending_approval",
    "resolvedApprovalMode": "manual",
    "timeBoxedExpiresAt": null,
    "timeBoxedExpiry": "execute",
    "dispatches": [
      {
        "index": 0,
        "type": "send_message_as_assistant",
        "platform": "email",
        "from": "ava@assistants.harborview.co",
        "to": ["legal@brightlinefreight.com"],
        "cc": ["jane@harborview.co"],
        "bcc": [],
        "subject": "Harborview service agreement renewal, 2027 terms",
        "body": "Hi Dana,\n\nOur service agreement with Brightline Freight ends on January 1, 2027. Harborview would like to renew. Could you send your proposed 2027 terms and rates by November 20? The current agreement is attached for reference.\n\nBest,\nAva, on behalf of Jane Rivera",
        "description": "Email Brightline Freight legal asking for 2027 renewal terms",
        "payload": null,
        "files": [
          {
            "filename": "Brightline-MSA-2026.pdf",
            "mimeType": "application/pdf",
            "size": 482113,
            "fileId": "6702c804b5c6d7e8f9a0b1c2"
          }
        ],
        "editedByUser": false,
        "currentVersionId": null,
        "validUntil": null,
        "evergreen": false
      }
    ],
    "contentHash": "cv1:3b8f1c2d9e4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c",
    "createdAt": "2026-11-02T16:25:02.000Z",
    "updatedAt": "2026-11-02T16:25:04.000Z"
  }
}
```

Approve exactly that content:

```bash
curl https://api.agnt.ai/portal/carts/6702c3a9b8c7d6e5f4a3b2c1/approve \
  -X POST \
  -H "Authorization: Bearer $AGNT_API_KEY" \
  -H "X-User-Email: jane@harborview.co" \
  -H "Content-Type: application/json" \
  -d '{ "contentHash": "cv1:3b8f1c2d9e4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c" }'
```

```json
{ "ok": true, "cartId": "6702c3a9b8c7d6e5f4a3b2c1", "status": "approved" }
```

| Name | Type | Required | Description |
|---|---|---|---|
| `contentHash` | string | Yes | The `contentHash` from the read you showed the user. |
| `reason` | string | No | A note recorded with the approval. |

Call approve only when the person has chosen to approve in your UI. Never approve from your backend on their behalf.

| Error | Status | Meaning |
|---|---|---|
| `content_hash_required` | `400` | `contentHash` is missing. |
| `cart_already_resolved` | `400` | The cart is no longer awaiting approval. |
| `cart_task_closed` | `400` | The task closed and its draft was withdrawn. |
| `cart_content_changed` | `409` | The content changed since it was read. Read it again and show the user. |

### Edit, remove or cancel

```bash
# Edit a staged message's subject or body. The contentHash changes.
curl https://api.agnt.ai/portal/carts/6702c3a9b8c7d6e5f4a3b2c1/dispatches/0/edit \
  -X PATCH \
  -H "Authorization: Bearer $AGNT_API_KEY" \
  -H "X-User-Email: jane@harborview.co" \
  -H "Content-Type: application/json" \
  -d '{ "subject": "Harborview service agreement renewal, 2027 terms (reply by Nov 20)" }'

# Remove one action from the cart.
curl -X DELETE https://api.agnt.ai/portal/carts/6702c3a9b8c7d6e5f4a3b2c1/dispatches/0 \
  -H "Authorization: Bearer $AGNT_API_KEY" \
  -H "X-User-Email: jane@harborview.co"

# Cancel the whole cart. The task is told and decides what to do next.
curl https://api.agnt.ai/portal/carts/6702c3a9b8c7d6e5f4a3b2c1/cancel \
  -X POST \
  -H "Authorization: Bearer $AGNT_API_KEY" \
  -H "X-User-Email: jane@harborview.co" \
  -H "Content-Type: application/json" \
  -d '{ "reason": "Wait until the Q1 2027 board meeting prep is done" }'
```

The edit endpoint returns the updated cart with its new `contentHash`. It accepts `subject`, `body`, `validUntil` and `evergreen`. Recipients cannot be changed there: `to`, `cc`, `bcc` or `payload` return `400`. Edits are accepted only while the cart is `pending_approval` or `time_boxed`, and editing a `time_boxed` cart extends its countdown.

### Undo

```bash
# What can still be undone, and until when.
curl https://api.agnt.ai/portal/carts/6702c3a9b8c7d6e5f4a3b2c1/snapshots \
  -H "Authorization: Bearer $AGNT_API_KEY" \
  -H "X-User-Email: jane@harborview.co"
```

```json
{
  "cartId": "6702c3a9b8c7d6e5f4a3b2c1",
  "count": 1,
  "snapshots": [
    {
      "dispatchIndex": 0,
      "dispatchType": "send_message_as_assistant",
      "resourceType": "email",
      "reversible": false,
      "undoStatus": "available",
      "undoExpiresAt": "2026-11-02T16:31:30.000Z",
      "undoneAt": null,
      "undoAvailable": true
    }
  ]
}
```

```bash
# Undo one action.
curl -X POST https://api.agnt.ai/portal/carts/6702c3a9b8c7d6e5f4a3b2c1/dispatches/0/undo \
  -H "Authorization: Bearer $AGNT_API_KEY" \
  -H "X-User-Email: jane@harborview.co"

# Undo every reversible action, in reverse order.
curl -X POST https://api.agnt.ai/portal/carts/6702c3a9b8c7d6e5f4a3b2c1/undo \
  -H "Authorization: Bearer $AGNT_API_KEY" \
  -H "X-User-Email: jane@harborview.co"
```

A reversible action, such as a created calendar event, is reverted. An irreversible action, such as a sent email, can be cancelled only while it is inside its `undoDelaySec` hold. After `undoExpiresAt`, undo returns an error.

### Tasks the assistant proposes

An assistant can propose a task instead of starting it. A proposed task has `createdBy: "agent"` and `approvalGate: "pending_user_approval"`, and does not run until approved. List proposed tasks with `GET /tasks?approvalGate=pending_user_approval`. Approve one with `POST /tasks/:taskId/approve`, decline it with `POST /tasks/:taskId/decline` (optional `{ "reason" }`), or approve up to 100 with `POST /tasks/approve-batch` and `{ "taskIds": [] }`.

### Kill switch

The account-wide kill switch freezes every outbound action for every user at once. While frozen, staged actions are refused, task creation and runs return `423` with `error_code: "KILL_SWITCH_ENGAGED"`, and inbound messages are not processed.

A person signed in to your hosted app freezes and releases the account. The console, API keys, certificate JWTs and service tokens get `403` (`KILL_SWITCH_RELEASE_FORBIDDEN`), so a leaked credential cannot release a freeze. Your backend can read the state:

```bash
curl https://api.agnt.ai/portal/account/kill-switch \
  -H "Authorization: Bearer $AGNT_API_KEY"
```

```json
{ "state": "active", "engagedAt": null, "engagedBy": null, "reason": null }
```

`state` is `frozen` while engaged and `active` otherwise. To stop a single task, use `POST /tasks/:taskId/stop`.

## Example: renew the Brightline Freight contract

Harborview's service agreement with Brightline Freight ends on 2027-01-01. Jane asks Ava to get the 2027 terms. Harborview requires approval for every email Ava sends.

1. **Require approval for assistant email.**

   ```bash
   curl https://api.agnt.ai/users/6702b1e8f9a0b1c2d3e4f5a6/preferences/supervision/matrix \
     -X PATCH \
     -H "Authorization: Bearer $AGNT_API_KEY" \
     -H "Content-Type: application/json" \
     -d '{ "matrix": { "email_as_assistant": { "mode": "manual" } } }'
   ```

2. **Create and start the task.** Create it as in [Create and run a task](#create-and-run-a-task), then start it with the MSA attached. The task id is `6702c3a9b8c7d6e5f4a3b2c1`.

3. **Ava stages the email.** Ava reads `Brightline-MSA-2026.pdf`, drafts an email to legal@brightlinefreight.com, and stages it. The task shows the cart:

   ```bash
   curl https://api.agnt.ai/tasks/6702c3a9b8c7d6e5f4a3b2c1 \
     -H "Authorization: Bearer $AGNT_API_KEY" \
     -H "X-User-Email: jane@harborview.co"
   ```

   ```json
   {
     "ok": true,
     "task": {
       "id": "6702c3a9b8c7d6e5f4a3b2c1",
       "title": "Renew the Brightline Freight service contract",
       "status": "executing",
       "effectiveApprovalMode": "manual",
       "pendingCart": {
         "status": "pending_approval",
         "label": "Email Brightline Freight legal for 2027 renewal terms",
         "timeBoxedExpiresAt": null,
         "timeBoxedExpiry": null,
         "dispatches": [
           {
             "type": "send_message_as_assistant",
             "platform": "email",
             "to": ["legal@brightlinefreight.com"],
             "subject": "Harborview service agreement renewal, 2027 terms",
             "resolvedApprovalMode": "manual"
           }
         ]
       }
     }
   }
   ```

4. **Jane approves it in your app.** Your app reads the cart, shows Jane the email and the attachment, and when she taps Approve, sends the `contentHash` it showed her (see [Review and approve](#review-and-approve)):

   ```bash
   curl https://api.agnt.ai/portal/carts/6702c3a9b8c7d6e5f4a3b2c1/approve \
     -X POST \
     -H "Authorization: Bearer $AGNT_API_KEY" \
     -H "X-User-Email: jane@harborview.co" \
     -H "Content-Type: application/json" \
     -d '{ "contentHash": "cv1:3b8f1c2d9e4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c" }'
   ```

5. **The email goes out and the task waits.** `status` becomes `awaiting_non_user_response`, `waitingOnContacts` lists Dana Okafor, and `alarm.wakeAt` is set so Ava follows up if Brightline does not reply.

6. **Dana replies with the 2027 terms.** The reply is attached to the task and wakes it. Your app makes no call.

7. **The task resumes.** Ava compares the proposed terms with the 2026 agreement and asks Jane to confirm a 3% rate increase. `status` becomes `awaiting_user_input`. Jane answers with `POST /tasks/6702c3a9b8c7d6e5f4a3b2c1/process`, and the task continues.

Follow every step with `GET /tasks/6702c3a9b8c7d6e5f4a3b2c1/activities`.

## Endpoints

| Method | Path | Description |
|---|---|---|
| `POST` | `/tasks` | Create a task. |
| `GET` | `/tasks` | List tasks. Filter by `status`, `type`, `assistant`, `approvalGate` or `q`. |
| `GET` | `/tasks/:taskId` | Get a task. |
| `PUT` | `/tasks/:taskId` | Update a task, including `approvalOverride`. |
| `DELETE` | `/tasks/:taskId` | Delete a task. |
| `POST` | `/tasks/:taskId/process` | Start a run, or send the task a message. |
| `GET` | `/tasks/:taskId/activities` | The timeline, newest first. |
| `POST` | `/tasks/:taskId/stop` | Stop the current run. Status becomes `on_hold`. |
| `POST` | `/tasks/:taskId/mark-done` | Mark done. |
| `POST` | `/tasks/:taskId/mark-not-done` | Reopen a done task. |
| `POST` | `/tasks/:taskId/feedback` | Like, dislike or clear. The value is accepted but not yet stored. |
| `POST` | `/tasks/:taskId/approve` | Approve a proposed task. |
| `POST` | `/tasks/:taskId/decline` | Decline a proposed task. |
| `POST` | `/tasks/approve-batch` | Approve several proposed tasks. |
| `POST` | `/skills` | Create a workflow (`kind: "workflow"`). |
| `GET` | `/users/:userId/preferences/supervision/matrix` | Read a user's autonomy settings and locks. |
| `PATCH` | `/users/:userId/preferences/supervision/matrix` | Change cells. |
| `PUT` | `/users/:userId/preferences/supervision/matrix/reset` | Clear the user's cells and exceptions. |
| `POST` | `/users/:userId/preferences/supervision/exceptions` | Add a recipient exception. |
| `DELETE` | `/users/:userId/preferences/supervision/exceptions/:exceptionId` | Remove a recipient exception. |
| `PATCH` | `/organizations/:orgId/settings/resource-permissions` | Organization cells and locks. |
| `PATCH` | `/teams/:teamId/settings/resource-permissions` | Team cells and locks. |
| `GET` | `/portal/carts` | Carts awaiting review. |
| `GET` | `/portal/carts/:cartId` | One cart and its `contentHash`. |
| `PATCH` | `/portal/carts/:cartId/dispatches/:index/edit` | Edit a staged action's text. |
| `DELETE` | `/portal/carts/:cartId/dispatches/:index` | Remove a staged action. |
| `POST` | `/portal/carts/:cartId/approve` | Approve. |
| `POST` | `/portal/carts/:cartId/cancel` | Cancel. |
| `GET` | `/portal/carts/:cartId/snapshots` | What can be undone. |
| `POST` | `/portal/carts/:cartId/dispatches/:index/undo` | Undo one action. |
| `POST` | `/portal/carts/:cartId/undo` | Undo every reversible action. |
| `GET` | `/portal/account/kill-switch` | Read the kill switch state. |

The SDK covers `tasks.create`, `list`, `get`, `update`, `delete`, `process`, `stop`, `feedback` and `updateAssignees`. Autonomy settings, carts and the kill switch are REST only.

## Limits

- `GET /portal/carts` returns at most 50 carts and is not paginated. Filter with `status=pending_approval,time_boxed`.
- `GET /tasks` pages with `page` and `perPage`. An account key without `X-User-Email` lists every user's tasks and narrows them with `ownerEmail`.
- `modelTier` can be changed only before a task's first run.
- Exception `timeBoxedWindowMin` is 1 to 60 minutes, and `undoDelaySec` is 0 to 300 seconds.
- Followers of a shared task can read its cart. Only the owner can approve, edit, cancel or undo it.

## See also

- [Prime](/reference/guides/prime): the harness that runs every task
- [Authentication](/reference/guides/authentication): API keys, `X-User-Email` and certificate JWTs
- [Users, organizations and teams](/reference/guides/organizations-and-teams): organization and team policy
- [Security](/reference/guides/security): how approvals, the kill switch and isolation fit together

---

# Users, organizations and teams

Source: https://agnt.ai/reference/guides/organizations-and-teams

Your account holds every end user of your app. Group users into organizations (your customer companies) and teams to give each customer its own admins, sign-in, directory sync, audit trail and policy. Data isolation does not follow this hierarchy: it is per user at every level.

## How it works

```text
Account          your app; holds every user, organization and setting.
 |                Console settings are the policy ceiling for everything below.
 +- Organization  a customer company (optional): admins, verified domains,
 |   |            SAML SSO, SCIM, audit export, skill library, model tiers
 |   +- Team      a group of users; team admins set policy the org left open
 |       +- User  one end user; owns tasks, chats, memories, contacts,
 |                connections and a workspace
 +- Team          a team outside any organization
     +- User
```

| Concept | Rule |
|---|---|
| Isolation | A user's data is visible to that user and to your account-wide credentials, never to other users. Sharing an account, organization or team grants nothing by itself. Sharing is explicit, for example a team-scoped memory or a task collaborator. |
| One active team | A user has exactly one active team membership. Adding a user to a team archives their other memberships. |
| Organization membership | A user belongs to at most one organization, with `organizationRole` `admin`, `member` or `guest`. |
| Team roles | `owner` (exactly one per team), `admin`, `member`. |
| Policy | A tier can change a setting only where no tier above it has locked it. See [Policy](#policy). |

Organization and team admins act through the hosted app (the end-user app served for your account) or through the API with their own credentials. The examples in this guide use an account-wide API key, which can administer every organization and team in your account. See [Authentication](/reference/guides/authentication) for key levels.

Your app's hosted domain is the domain the hosted app is served on: its AGNT subdomain or your custom domain. SAML sign-in and SCIM are served there.

## Create an organization

Create an organization for each customer company.

```bash
curl https://api.agnt.ai/organizations \
  -X POST \
  -H "Authorization: Bearer $AGNT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "name": "Harborview Property Management" }'
```

The response returns the organization with `"id": "6702a9c3e1b4f5d6a7b8c901"`.

`PUT /organizations/:orgId` sets `name`, `defaultTeam` (where organization-scoped provisioning places new users) and `defaultAssistant`. `DELETE /organizations/:orgId` deletes the organization and its domains. Its users and teams stay in your account, and teams left with no members are deleted.

## Verify a domain

A verified domain proves the customer controls it. It lets the organization claim its users: auto-join, SSO sign-in, and the rule that users at that domain can join only that organization's teams.

```bash
curl https://api.agnt.ai/organizations/6702a9c3e1b4f5d6a7b8c901/domains \
  -X POST \
  -H "Authorization: Bearer $AGNT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "domain": "harborview.co", "autoJoin": true }'
```

```json
{
  "ok": true,
  "domain": {
    "id": "6702aa14b7c8d9e0f1a2b3c4",
    "domain": "harborview.co",
    "source": "manual",
    "verified": false,
    "autoJoin": true,
    "txtRecordName": "_agnt-verification.harborview.co",
    "txtRecordValue": "agnt-06815aa2c88aa0194d3af3d4476fe650ae6cd63017d0e66d6a4ae065",
    "verifiedAt": null,
    "lastCheckedAt": null,
    "createdAt": "2026-11-02T16:24:11.000Z",
    "updatedAt": "2026-11-02T16:24:11.000Z"
  }
}
```

Publish a DNS TXT record named `txtRecordName` with the value `txtRecordValue`, then verify:

```bash
curl https://api.agnt.ai/organizations/6702a9c3e1b4f5d6a7b8c901/domains/6702aa14b7c8d9e0f1a2b3c4/verify \
  -X POST \
  -H "Authorization: Bearer $AGNT_API_KEY"
```

The response returns the domain with `verified: true` and `verifiedAt` set. Until the record is visible in DNS, verify returns `400` and records `lastCheckedAt`. Call it again after propagation.

| Name | Type | Required | Description |
|---|---|---|---|
| `domain` | string | Yes | The domain, such as `harborview.co`. Stored lower-cased. |
| `autoJoin` | boolean | No | Default `false`. Once the domain is verified, users with an email at this domain join the organization as `member` when they are provisioned or sign up. |

`PUT /organizations/:orgId/domains/:domainId` with `{ autoJoin }` changes auto-join. Domains added by an SSO configuration have `source: "sso"` and cannot be edited or removed directly.

## Provision users

`POST /users/provision` finds or creates a user by email and places them on a team. It is idempotent, so call it on every sign-in. It returns `201` when it creates the user and `200` when the user exists.

Provision Maria Chen onto the Leasing team:

```bash
curl https://api.agnt.ai/users/provision \
  -X POST \
  -H "Authorization: Bearer $AGNT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "maria@harborview.co",
    "firstName": "Maria",
    "lastName": "Chen",
    "teamId": "6702ab52c3d4e5f6a7b8c9d0"
  }'
```

```json
{
  "ok": true,
  "user": {
    "id": "6702b23a0b1c2d3e4f5a6b7c",
    "account": "66f1a2b3c4d5e6f708192a3b",
    "email": "maria@harborview.co",
    "assistant": null,
    "name": "Maria Chen",
    "firstName": "Maria",
    "lastName": "Chen",
    "avatarUrl": null,
    "timezone": "UTC",
    "locale": "en-US",
    "organization": "6702a9c3e1b4f5d6a7b8c901",
    "organizationRole": "member",
    "team": {
      "id": "6702ab52c3d4e5f6a7b8c9d0",
      "name": "Leasing",
      "organization": "6702a9c3e1b4f5d6a7b8c901",
      "status": "active"
    },
    "teamRole": "member",
    "status": "active",
    "createdAt": "2026-11-02T16:24:11.000Z",
    "updatedAt": "2026-11-02T16:24:11.000Z"
  },
  "teamId": "6702ab52c3d4e5f6a7b8c9d0",
  "created": true,
  "addedToTeam": true,
  "alreadyMember": false,
  "joinSkipped": false
}
```

Maria joined Harborview Property Management because `harborview.co` is a verified auto-join domain.

| Name | Type | Required | Description |
|---|---|---|---|
| `email` | string | Yes | Lower-cased and trimmed. Becomes the user's first identifier, so `X-User-Email` resolves to them. |
| `firstName`, `lastName` | string | No | Used in the assistant's introductions and signatures. |
| `teamId` | string | No | An existing team in your account. |
| `id` | string | No | Your own id for the user, used only if it is a 24-character hex string. |

Where the user lands:

| Request | Team | Organization |
|---|---|---|
| `teamId` given | That team, as `member`. If the email's domain is verified by an organization, the team must belong to that organization, or the call returns `403`. | As below, by key type |
| No `teamId`, organization-scoped key | The organization's `defaultTeam`, or a new team in the organization owned by the user | The key's organization |
| No `teamId`, account-wide key | A new team owned by the user | The organization that verified the email's domain with `autoJoin`, if any |

| Response field | Description |
|---|---|
| `created` | A new user was created. |
| `addedToTeam` | A new team membership was created. |
| `alreadyMember` | The user and the membership already existed. Nothing changed. |
| `joinSkipped` | The user owns another team that still has active members, so they were not moved. Transfer that team's ownership, then call again. |

A new user needs a timezone and working hours before a key can act as them. See the [Quickstart](/reference/guides/quickstart).

### Update a user

`PUT /users/:userId` changes a user's profile, organization, role and status. Make Jane Rivera an admin of Harborview:

```bash
curl https://api.agnt.ai/users/6702b1e8f9a0b1c2d3e4f5a6 \
  -X PUT \
  -H "Authorization: Bearer $AGNT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "organization": "6702a9c3e1b4f5d6a7b8c901", "organizationRole": "admin" }'
```

| Name | Type | Description |
|---|---|---|
| `organization` | string or null | An organization id, or `null` to remove the user from their organization. |
| `organizationRole` | string | `admin`, `member` or `guest`. |
| `status` | string | `active`, `inactive` or `suspended`. While `inactive` or `suspended`, every request acting as the user fails with `403 account_blocked`. |
| `firstName`, `lastName`, `name`, `timezone`, `locale`, `metadata` | | Profile fields. |
| `externalId` | string | Your id for the user. Unique in the account. |

Set `status` to `suspended` to cut off a user's access without deleting anything. An organization admin can change these fields only for users in their own organization.

### Add people to an organization

| Method | Use when |
|---|---|
| Verified domain with `autoJoin: true` | Everyone at the customer's domain belongs to the customer. |
| `PUT /users/:userId` with `organization` | Your backend decides membership. |
| SAML just-in-time sign-in, or SCIM | The customer's identity provider decides. |
| `POST /organizations/:orgId/members` with `{ email }` | An admin adds one person. The person is notified by email and in the app. Provision the user first. |

`DELETE /organizations/:orgId/members/:userId` removes a user from the organization and its teams.

## Teams

Create the Leasing team inside Harborview by calling as Jane, an organization admin. Jane becomes the owner and moves from her current team.

```bash
curl https://api.agnt.ai/teams \
  -X POST \
  -H "Authorization: Bearer $AGNT_API_KEY" \
  -H "X-User-Email: jane@harborview.co" \
  -H "Content-Type: application/json" \
  -d '{ "name": "Leasing" }'
```

The response returns the team with `"id": "6702ab52c3d4e5f6a7b8c9d0"` and `"organization": "6702a9c3e1b4f5d6a7b8c901"`.

A team belongs to the caller's organization: an organization admin's, or an organization-scoped key's. Called with an account-wide key and no `X-User-Email`, the team belongs to no organization, and `ownerId` (a user in your account) is required.

Add a user with `POST /teams/:teamId/members`. The user moves from their current team. Move Maria to the Maintenance team:

```bash
curl https://api.agnt.ai/teams/6702ab9fd4e5f6a7b8c9d0e1/members \
  -X POST \
  -H "Authorization: Bearer $AGNT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "userId": "6702b23a0b1c2d3e4f5a6b7c", "role": "member" }'
```

| Name | Type | Required | Description |
|---|---|---|---|
| `userId` | string | Yes | The user to add. |
| `role` | string | No | `member` (default) or `admin`. |
| `newOwnerId` | string | No | If the user owns their current team and it has other active members, an active member of that team to hand it to. Without it the call returns `400` with `requiresOwnershipTransfer: true` and that `teamId`. |

- Adding a user to an organization's team makes them a member of that organization if they had none. A user in a different organization is refused with `403`.
- If the user was the last member of their old team, that team is archived.
- The response returns `member.id`, the membership id. Use it as `memberId` below. `GET /teams/:teamId/members` lists memberships.

| Endpoint | Rule |
|---|---|
| `PUT /teams/:teamId/members/:memberId` | `role`: `member` or `admin`. `status`: `active`, `invited` or `archived`. A team admin cannot change the owner, grant `owner`, or change another admin except to `member`. |
| `POST /teams/:teamId/transfer-ownership` | `{ newOwnerId }`, called as the current owner with `X-User-Email`. The new owner must be an active member. The old owner becomes `member`. |
| `DELETE /teams/:teamId/members/:memberId` | The owner cannot be removed, and admins cannot remove admins. The removed user moves to another active team, else a team they previously owned, else a new team of their own. |

## Deprovision users

`POST /users/deprovision` removes a user from one team. If the user then has no team membership at all, active or archived, the user record and their identifiers, integrations and chats are deleted. For the full cascade use `DELETE /users/:userId`.

```bash
curl https://api.agnt.ai/users/deprovision \
  -X POST \
  -H "Authorization: Bearer $AGNT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "email": "maria@harborview.co", "teamId": "6702ab9fd4e5f6a7b8c9d0e1" }'
```

```json
{ "ok": true, "deleted": true, "userDeleted": true, "userId": "6702b23a0b1c2d3e4f5a6b7c" }
```

- `email` and `teamId` are required. The team must be in your account, and in the key's organization for an organization-scoped key, or the call returns `404`.
- An email that matches no user returns `200` with `deleted: false`.
- `userDeleted: false` means the user still has another membership, so the user record and its data remain.

Deprovisioning is refused, with nothing changed, when it would leave something without an administrator:

| Status | Flag | When | Resolve by |
|---|---|---|---|
| `409` | `requiresOrgAdminTransfer: true`, `organizationId` | The user is the organization's only `admin` | Promote another user with `PUT /users/:userId` |
| `400` | `requiresOwnershipTransfer: true`, `teamId` | The user owns a team with other active members | `POST /teams/:teamId/transfer-ownership` |

`DELETE /users/:userId` deletes a user outright, whatever their memberships, with the same two refusals. Deletion runs asynchronously: the response confirms it is scheduled. `DELETE /organizations/:orgId/members/:userId` also returns `409 requiresOrgAdminTransfer` for the only admin.

## SAML SSO

Each organization can sign its users in through its own SAML 2.0 identity provider (IdP), such as Okta, Microsoft Entra ID or Google Workspace. A user enters their email on your app's hosted domain. If the domain is verified by an organization with SSO enabled, the user is sent to that organization's IdP.

1. Register the service provider in the IdP:

   | IdP setting | Value |
   |---|---|
   | ACS (reply) URL | `https://<your app's hosted domain>/api/sso/saml/acs` |
   | SP entity ID and metadata | `https://<your app's hosted domain>/api/sso/saml/metadata` |
   | Single logout URL | `https://<your app's hosted domain>/api/sso/saml/slo` |
   | Attributes | Email, first name, last name and, optionally, groups |

2. Configure the organization with the IdP's details:

   ```bash
   curl https://api.agnt.ai/organizations/6702a9c3e1b4f5d6a7b8c901/sso \
     -X PUT \
     -H "Authorization: Bearer $AGNT_API_KEY" \
     -H "Content-Type: application/json" \
     -d '{
       "enabled": true,
       "enforced": true,
       "provider": "saml_generic",
       "saml": {
         "entryPoint": "https://harborview.okta.com/app/harborview_agnt/exk7q2m9/sso/saml",
         "certificate": "MIIDqDCCApCgAwIBAgIGAYk3HarborviewSigningCert",
         "attributeMapping": { "email": "email", "firstName": "givenName", "lastName": "sn", "groups": "groups" },
         "groupFilter": { "enabled": true, "allowedGroups": ["harborview-staff"] },
         "groupRoleMapping": [ { "group": "harborview-it", "role": "admin" } ]
       },
       "memberSync": { "autoProvision": true }
     }'
   ```

| Name | Type | Description |
|---|---|---|
| `enabled` | boolean | Turns SSO sign-in on for the organization's verified domains. |
| `enforced` | boolean | Users at those domains, and members of the organization, can sign in only through SSO. Other sign-in methods return `403 sso_required`. |
| `provider` | string | `saml_generic`. Every SAML 2.0 IdP connects through it. |
| `saml.entryPoint` | string | The IdP's SSO URL. Required, with `certificate`, before sign-in works. |
| `saml.certificate` | string | The IdP's signing certificate. Every response is verified against it. Never returned. |
| `saml.issuer` | string | The IdP's entity ID, stored for your reference. |
| `saml.signatureAlgorithm` | string | Default `sha256`. |
| `saml.attributeMapping` | object | Attribute names for `email`, `firstName`, `lastName` and `groups`. Defaults to those names. The email falls back to the NameID. |
| `saml.groupFilter` | object | `{ enabled, allowedGroups, requireAll }`. Only users in any of these groups (all of them, with `requireAll`) may sign in. Case-insensitive. |
| `saml.groupRoleMapping` | array | `[{ group, role }]` with `role` `admin` or `member`. Sets the user's organization role at each sign-in. The first matching group wins. |
| `saml.allowIdpInitiated` | boolean | Default `false`. When `true`, sign-ins started from the IdP's dashboard are accepted. |
| `memberSync.autoProvision` | boolean | `true` creates a user on first sign-in. `false` admits only users you have already provisioned. |

SSO guarantees:

- Assertions must be signed. An unsigned assertion, or one that fails verification against `certificate`, is rejected.
- An assertion must be addressed to your ACS URL, be within its validity window, and is accepted once for IdP-initiated sign-in. SP-initiated sign-in must complete within 10 minutes of starting.
- The signed-in email must belong to one of the organization's verified domains.

`PUT` merges each object you send into the stored configuration, so you can rotate `saml.certificate` alone. `DELETE /organizations/:orgId/sso` clears the SSO configuration, including the SCIM token.

## SCIM 2.0

SCIM lets the customer's IdP create, update and deactivate users and manage teams. It works with or without SAML SSO. Mint the organization's token:

```bash
curl https://api.agnt.ai/organizations/6702a9c3e1b4f5d6a7b8c901/sso/scim-token \
  -X POST \
  -H "Authorization: Bearer $AGNT_API_KEY"
```

```json
{ "ok": true, "scim": { "token": "9b1e0c4f7a2d8e3b6c5a1f0d9e8b7c6a" } }
```

The token is shown once, and only a hash is stored. Each organization has one token: minting a new one invalidates the previous one immediately. Give the IdP the token as a bearer token and this base URL on your app's hosted domain:

```text
https://<your app's hosted domain>/api/scim/v2
```

| Method | Path | Behavior |
|---|---|---|
| GET | `/Users` | Users in the organization. Supports `startIndex`, `count` (max 200) and `filter=userName eq "maria@harborview.co"`. |
| POST | `/Users` | Creates a user in the organization from `userName` (or the first of `emails`) and `name`. Returns `409` if the email exists in the account. |
| GET | `/Users/:id` | One user. |
| PUT, PATCH | `/Users/:id` | Updates `name.givenName`, `name.familyName` and `active`. `active: false` blocks the user. |
| DELETE | `/Users/:id` | Deactivates the user and keeps their data. [Deprovision](#deprovision-users) to delete it. |
| GET, POST | `/Groups` | Teams in the organization. `POST` creates a team from `displayName`. |
| GET, PUT, PATCH, DELETE | `/Groups/:id` | Renames a team, or adds, removes and replaces members. `DELETE` deletes the team and archives its memberships. |
| GET | `/ServiceProviderConfig` | Patch and filter are supported. Bulk and password change are not. |

Responses use `application/scim+json`. User `id`s are AGNT user ids and `userName` is the email. Adding a user to a group moves them from their current team. A user in a different organization is skipped, as is a user who owns another team that still has active members.

## Audit events

Administrative actions are recorded as audit events: organization, domain and SSO changes, membership and role changes, team policy changes, audit-export changes and SSO sign-ins. Actions by a signed-in user, or an API key acting as one with `X-User-Email`, who belongs to a team in an organization with audit logging enabled are recorded. Account-wide key calls without `X-User-Email` and SCIM changes are not. Events are archived nightly. The API serves the most recent 30 days.

```bash
curl "https://api.agnt.ai/organizations/6702a9c3e1b4f5d6a7b8c901/audit-events?action=sso.updated&after=2026-11-01T00:00:00Z" \
  -H "Authorization: Bearer $AGNT_API_KEY"
```

```json
{
  "ok": true,
  "events": [
    {
      "id": "6702d1a8e9f0a1b2c3d4e5f6",
      "account": "66f1a2b3c4d5e6f708192a3b",
      "organization": "6702a9c3e1b4f5d6a7b8c901",
      "actor": "jane@harborview.co",
      "action": "sso.updated",
      "entityType": "organization",
      "entityId": "6702a9c3e1b4f5d6a7b8c901",
      "metadata": { "provider": "saml_generic", "enabled": true, "enforced": true },
      "createdAt": "2026-11-02T16:24:11.000Z"
    }
  ],
  "page": 1,
  "perPage": 20,
  "total": 1
}
```

| Name | Type | Description |
|---|---|---|
| `actor` | string | The actor's email. Exact match. |
| `action` | string | Such as `sso.updated` or `domain.verified`. |
| `entityType` | string | Such as `organization` or `domain`. |
| `after`, `before` | date-time | Time range. |
| `page`, `perPage` | integer | Newest first. `perPage` max 100. |

`GET /audit-events` returns events across your account and also accepts `entityId`. `GET /audit-events/:eventId` returns one event.

### Export audit events to S3

Deliver audit events to the customer's own S3 bucket to keep them beyond 30 days.

1. In the customer's AWS account, create an IAM role with this permissions policy:

   ```json
   {
     "Version": "2012-10-17",
     "Statement": [{
       "Effect": "Allow",
       "Action": "s3:PutObject",
       "Resource": "arn:aws:s3:::harborview-agnt-audit/agnt/*"
     }]
   }
   ```

   The role's trust policy must allow only the AGNT account that delivers audit exports. Contact support@agnt.ai for the account ID and the exact role to trust. Do not trust the account root. Use a dedicated bucket with versioning enabled.

2. Configure the organization:

   ```bash
   curl https://api.agnt.ai/organizations/6702a9c3e1b4f5d6a7b8c901/audit-logging \
     -X PUT \
     -H "Authorization: Bearer $AGNT_API_KEY" \
     -H "Content-Type: application/json" \
     -d '{
       "enabled": true,
       "s3BucketArn": "arn:aws:s3:::harborview-agnt-audit",
       "s3Prefix": "agnt",
       "roleArn": "arn:aws:iam::210987654321:role/agnt-audit-writer",
       "region": "us-west-2"
     }'
   ```

   The response returns the stored configuration with `"deliveryConfigured": true`.

3. Deliver a time range. The call returns `202` with a delivery job:

   ```bash
   curl https://api.agnt.ai/organizations/6702a9c3e1b4f5d6a7b8c901/audit-logging/deliver \
     -X POST \
     -H "Authorization: Bearer $AGNT_API_KEY" \
     -H "Content-Type: application/json" \
     -d '{ "from": "2026-11-01T00:00:00Z", "to": "2026-11-02T00:00:00Z" }'
   ```

4. Read the job's status with `GET /organizations/:orgId/audit-logging/deliver/:jobId`:

   ```json
   {
     "ok": true,
     "deliveryJob": {
       "id": "6702d2b9f0a1b2c3d4e5f6a7",
       "organization": "6702a9c3e1b4f5d6a7b8c901",
       "requestedBy": "jane@harborview.co",
       "status": "completed",
       "from": "2026-11-01T00:00:00.000Z",
       "to": "2026-11-02T00:00:00.000Z",
       "filesCopied": 1,
       "deliveryErrors": [],
       "completedAt": "2026-11-02T16:25:03.000Z",
       "createdAt": "2026-11-02T16:24:11.000Z"
     }
   }
   ```

| Name | Type | Description |
|---|---|---|
| `enabled` | boolean | Turns audit logging on for the organization. Delivery returns `403` while it is `false`. |
| `s3BucketArn` | string | The destination bucket. Delivery returns `400` until both `s3BucketArn` and `roleArn` are set. |
| `s3Prefix` | string | Key prefix. Default `agnt-audit-logs`. |
| `roleArn` | string | The IAM role assumed to write. |
| `region` | string | The bucket's region. Default `us-east-1`. |

- Each delivery copies one range. Without `from` and `to`, it covers the 30 days up to 00:00 UTC today. To keep a continuous copy, call deliver on a schedule, for example daily.
- Files are JSON Lines, written to `<s3Prefix>/year=YYYY/month=MM/day=DD/`.
- Delivery is idempotent: delivering a range again overwrites the same objects with the same content.
- Job `status` is `pending`, `running`, `completed` or `failed`.

## Policy

You set the ceiling for your app in the AGNT console. Below it, each control is inherited, tightened or locked. A tier can change a control only where no tier above it has locked it.

| Control | Account (console) | Organization | Team |
|---|---|---|---|
| Skills | Store tiers offered; skills blocked or allowed app-wide; whether users can browse, install, create and share | Skill library: allowed tiers and enabled skills | Store on or off, allowlist, blocklist; create and share rules |
| MCP | `mcpAccessLevel` cap and lock; whether custom MCP servers can be added | | Its own cap and lock; custom MCP servers Inherit, On or Off |
| Connections | Allowed account types; access per resource with locks; connection catalog | | The same fields; each connection Inherit, On or Off |
| Computer use | Workspace switches with locks; restricted preset | | The same switches and preset |
| Autonomy | | Approval mode per action type, and locks | The same, except cells the organization locked |
| Model tiers | Default chat and task tiers | Default chat and task tiers | |
| Kill switch | Freezes every outbound action for the account | | |

How tiers combine:

- **Computer use and the restricted preset.** An `"off"` at the account or the user's team wins. With no setting anywhere, each switch is on. See [Computer use](/reference/guides/computer-use).
- **MCP and connection access.** The most restrictive level across account, team and user applies. A lock forces the admin's level.
- **Skill store.** A team with the store off wins. Blocklists add up across tiers. Allowlists narrow.
- **Skill library.** When an organization configures its library, a skill is available to its users only if its tier is in `allowedTiers` and its id is in `enabledSkills`. Skills the platform installs as locked are always available. An organization with no library configuration inherits everything.
- **Autonomy.** An organization lock pins the cell for every team and user in it. Modes, defaults and the order in which a mode is decided are in [Autonomy](/reference/guides/tasks-and-approvals#autonomy).
- **Connection catalog.** A team's Inherit, On or Off decides whether a connection is offered to the team's users in the hosted app. A team value takes precedence over the account's.

Account-tier settings are changed only by account admins signed in to the console. Organization and team settings accept an account-wide key or an admin of that organization or team.

### Set team policy

Cap MCP at read-only, pin calendar access to read-only, block one skill, and stop the Leasing team from adding its own MCP servers:

```bash
curl https://api.agnt.ai/teams/6702ab52c3d4e5f6a7b8c9d0/settings/resource-permissions \
  -X PATCH \
  -H "Authorization: Bearer $AGNT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "mcpAccessLevel": "read",
    "mcpLocked": true,
    "connections": {
      "allowedTypes": ["google", "azure"],
      "connectionPermissions": { "google": { "calendar": "read" }, "azure": { "calendar": "read" } },
      "connectionLocks": { "google": { "calendar": true }, "azure": { "calendar": true } }
    },
    "store": { "blocklist": ["6702c915c6d7e8f9a0b1c2d3"] }
  }'

curl https://api.agnt.ai/teams/6702ab52c3d4e5f6a7b8c9d0/settings/custom-mcp \
  -X PATCH \
  -H "Authorization: Bearer $AGNT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "allowCustomMcp": "off" }'
```

On team settings, `null` (or `"inherit"`) inherits and a value is an explicit setting. A lock pins a value so the user's own preference is ignored. A lock with no value is rejected.

| Name | Type | Description |
|---|---|---|
| `mcpAccessLevel` | string or null | `off`, `read` or `read_write`. |
| `mcpLocked` | boolean or null | Locks `mcpAccessLevel`. Requires a team value. |
| `connections.allowedTypes` | array or null | `null` inherits, `[]` allows all, or a list of account types. |
| `connections.connectionPermissions` | object | `{ <type>: { <resource>: "off" \| "read" \| "read_write" \| null } }` |
| `connections.connectionLocks` | object | `{ <type>: { <resource>: true \| false \| null } }`. A value is required before a lock. A locked value cannot be cleared without clearing its lock in the same request. |
| `capabilities` | object | Computer-use switches. See [Computer use](/reference/guides/computer-use). |
| `store` | object | `{ enabled, allowlist, blocklist }` of skill ids. `null` inherits. |
| `skills` | object | `canBrowseStore`, `canInstallFromStore`, `canCreate`, `requireApproval`, `canShareWithTeam`, `requirePermissionToShare`: `true`, `false` or `null`. |
| `users` | object | `createSkills`, `shareSkills`: `true`, `false` or `null`. |
| `approvalSettings.matrix` | object | Autonomy cells and `locked`. Cells the organization locked are ignored. |

Account types are `google`, `azure` (Microsoft), `apple`, `slack`, `zoom`, `asana`, `hubspot`, `imap`, or `mcp:<name>` for an MCP connection. Resources are `calendar`, `contacts`, `email`, `drive`, `messaging`, `video`, `teams`, `projects` and `crm`.

`GET /teams/:teamId/settings/resource-permissions` returns the team's stored values plus the organization's locked autonomy cells (`orgLockedCells`, `orgLockedValues`), so an admin UI can show what it cannot change.

To set a connection's catalog state for a team, send `{ "enabled": "inherit" | "on" | "off" }` to `PATCH /teams/:teamId/settings/connections/:connectionId`. `GET /teams/:teamId/settings/connections` lists each connection with `orgEnabled`, `teamEnabled` and `effectiveState`.

### Apply the restricted security preset

The restricted preset turns off and locks MCP servers (including custom ones), the workspace shell, browser automation and workspace handoff in one call. Apply it to the Maintenance team:

```bash
curl https://api.agnt.ai/teams/6702ab9fd4e5f6a7b8c9d0e1/settings/security-preset \
  -X POST \
  -H "Authorization: Bearer $AGNT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "preset": "restricted" }'
```

Apply it to the whole account in the AGNT console. See [Computer use](/reference/guides/computer-use) for what each switch covers.

### Set organization policy

Offer Harborview's users only first-party and official skills, and only the ones listed:

```bash
curl https://api.agnt.ai/organizations/6702a9c3e1b4f5d6a7b8c901/library \
  -X PATCH \
  -H "Authorization: Bearer $AGNT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "allowedTiers": ["agnt", "official"], "enabledSkills": ["6702c915c6d7e8f9a0b1c2d3"] }'
```

`allowedTiers` takes `agnt`, `official` and `community`. `bulkEnableTier` (one tier name) adds every published skill in that tier to `enabledSkills`.

To set and lock autonomy for the organization, use `PATCH /organizations/:orgId/settings/resource-permissions`. See [Autonomy](/reference/guides/tasks-and-approvals#autonomy).

### Default model tiers

Set the organization's default tiers for new chats and tasks. They apply between a task's own tier and your account's default.

```bash
curl https://api.agnt.ai/organizations/6702a9c3e1b4f5d6a7b8c901/settings/model-tiers \
  -X PATCH \
  -H "Authorization: Bearer $AGNT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "chat": "medium", "task": "high" }'
```

```json
{ "ok": true, "defaultModelTiers": { "chat": "medium", "task": "high" } }
```

Values are `low`, `medium`, `high`, `ultra`, or `null` to inherit your account's default. See [Model tiers](/reference/guides/prime#model-tiers).

## Endpoint reference

Every endpoint accepts an account-wide API key. Organization endpoints also accept an admin of that organization, and team endpoints an admin of that team.

| Method | Path | Description |
|---|---|---|
| `POST` | `/users/provision` | Find or create a user and place them on a team |
| `POST` | `/users/deprovision` | Remove a user from a team; delete the user if no membership remains |
| `GET` | `/users` | List users |
| `PUT` | `/users/:userId` | Update profile, `organization`, `organizationRole`, `status` |
| `DELETE` | `/users/:userId` | Delete a user and their data (asynchronous) |
| `GET` `POST` | `/teams` | List; create |
| `GET` `PUT` `DELETE` | `/teams/:teamId` | Read; update; delete |
| `GET` `POST` | `/teams/:teamId/members` | List; add |
| `PUT` `DELETE` | `/teams/:teamId/members/:memberId` | Change role or status; remove |
| `POST` | `/teams/:teamId/transfer-ownership` | Transfer ownership, as the current owner |
| `GET` `PATCH` | `/teams/:teamId/settings/resource-permissions` | Team policy |
| `POST` | `/teams/:teamId/settings/security-preset` | Apply the restricted preset |
| `GET` | `/teams/:teamId/settings/connections` | Connection catalog with account and team state |
| `PATCH` | `/teams/:teamId/settings/connections/:connectionId` | Set a connection's catalog state |
| `PATCH` | `/teams/:teamId/settings/custom-mcp` | Allow or block custom MCP servers |
| `GET` `POST` | `/organizations` | List; create |
| `GET` `PUT` `DELETE` | `/organizations/:orgId` | Read; update; delete |
| `GET` `POST` | `/organizations/:orgId/members` | List; add and notify |
| `DELETE` | `/organizations/:orgId/members/:userId` | Remove from the organization and its teams |
| `GET` `POST` | `/organizations/:orgId/domains` | List; add |
| `PUT` `DELETE` | `/organizations/:orgId/domains/:domainId` | Update auto-join; remove |
| `POST` | `/organizations/:orgId/domains/:domainId/verify` | Check the TXT record |
| `PUT` `DELETE` | `/organizations/:orgId/sso` | Configure (merge); clear |
| `POST` | `/organizations/:orgId/sso/scim-token` | Mint the SCIM token |
| `GET` `PATCH` | `/organizations/:orgId/settings/resource-permissions` | Autonomy matrix and locks |
| `GET` `PATCH` | `/organizations/:orgId/library` | Skill library |
| `GET` `PATCH` | `/organizations/:orgId/settings/model-tiers` | Default model tiers |
| `GET` | `/organizations/:orgId/audit-events` | The organization's audit events |
| `GET` `PUT` | `/organizations/:orgId/audit-logging` | S3 export configuration |
| `POST` | `/organizations/:orgId/audit-logging/deliver` | Start a delivery |
| `GET` | `/organizations/:orgId/audit-logging/deliver/:jobId` | Delivery status |
| `GET` | `/audit-events` | Account-wide audit events |
| `GET` | `/audit-events/:eventId` | One audit event |

SAML (`/api/sso/saml/acs`, `/api/sso/saml/metadata`, `/api/sso/saml/slo`) and SCIM (`/api/scim/v2`) are served on your app's hosted domain, not `api.agnt.ai`.

## Limits

- Audit events are served for 30 days. Export them to keep them longer.
- List endpoints page with `page` and `perPage` (default 20, max 100). SCIM pages with `startIndex` and `count` (default 100, max 200), filters only on `userName eq`, and has no bulk operations.
- Up to 10 organizations can be created per account per minute.
- SAML sign-in and SCIM serve users of the hosted app. If you build your own sign-in, provision and deprovision from your backend.
- Organization-scoped API keys act only within their organization, and with `X-User-Email` they are limited to organization admin operations. See [Authentication](/reference/guides/authentication).

## See also

- [Authentication](/reference/guides/authentication)
- [Tasks and approvals](/reference/guides/tasks-and-approvals)
- [Computer use](/reference/guides/computer-use)
- [Security](/reference/guides/security)
- [API reference](/reference/api)

---

# Security

Source: https://agnt.ai/reference/guides/security

AGNT assistants read a user's mail and calendar, message other people, place calls and operate a workspace (the user's cloud computer) on that user's behalf. Every user's data is isolated from every other user's, every credential is hashed or encrypted, and every type of consequential action can require a person's approval, with an account-wide kill switch above it. This guide states those guarantees and the controls you configure.

## Controls at a glance

| Area | Guarantee |
|---|---|
| Isolation | End-user data is isolated per user, not only per app. Missing context never grants access. |
| Credentials | API keys are stored only as hashes. Third-party tokens and secrets are encrypted with AES-256 at the application layer. All traffic uses TLS. |
| Human control | Every consequential action type can require approval, per user, with admin locks. An approval executes exactly the content shown. Actions can be undone within a window. A kill switch freezes all outbound actions. |
| Untrusted content | Email, web pages and documents are treated as data, not instructions. Unknown senders are gated. |
| Inbound integrity | Messaging webhooks (Slack, Twilio, WhatsApp) are verified by signature or shared secret and refused if the credential is missing. Calendar and mailbox change notifications are treated as untrusted pings and the data is re-fetched. Inbound email is authenticated before an assistant sees it. |
| Code execution | Plugins you author run in an isolated sandbox with no access to platform secrets. Each user's workspace runs in its own container with no public IP, and your commands, files and the browser run as an unprivileged user. |
| Identity | SAML SSO with signed assertions, SCIM provisioning, TOTP two-factor authentication for the console. |
| Audit | Administrative actions are recorded as audit events, archived nightly and exportable to your own S3 bucket. The API serves the last 30 days. |
| Compliance | SOC 2 Type II, audited annually. |
| Hosting | AWS in the United States. |

## User isolation

An account is your app on AGNT. It holds all of your end users, who are unrelated to each other, so data is isolated at the user boundary, not the account boundary.

- Tasks, chats, memories, contacts, calendars, approvals and workspaces belong to a user. A request acting as one user cannot read or change another user's.
- A user-scoped key acts only as its own user for approvals and edits.
- An account-wide API key is your server's trusted credential. It can list, read and delete tasks and chats across all of your users, for support and admin tooling. Keep it on your servers.
- An `X-User-Email` that does not resolve to a user in your account is refused. A failed scoping check never falls back to account-wide access.
- A certificate JWT authenticates only the account whose certificate signed it.
- Collaboration is explicit. Another person sees a task only when added to it as a collaborator, and only the task's owner can approve what it sends.

Organizations and teams group users for administration and policy. They do not share data. See [Users, organizations and teams](/reference/guides/organizations-and-teams).

## Credentials

| Credential | Handling |
|---|---|
| API keys (`ak_live_...`) | Shown once at creation. Only a hash is stored. Revocation is immediate. Keys are account-, organization- or user-scoped and can be labelled with scopes (not yet enforced). |
| Certificate JWTs | RS256, verified against the public key registered for your account in the AGNT console, by `kid`. Used by `@agnt-sdk/client`. |
| Connected-account tokens | OAuth tokens for users' Google, Microsoft and other accounts are encrypted with AES-256 at the application layer. |
| Integration credentials and webhook secrets | Encrypted with AES-256 at the application layer. Webhook signing secrets are shown once. |

Give a client that acts for one user a user-scoped key, and revoke keys you no longer need. See [Authentication](/reference/guides/authentication) for creating, scoping and revoking keys.

## Human approval and the kill switch

Autonomy is set per user and per action type: email as the user, email as the assistant, messaging, calls, calendar, drive, files, contacts, connected apps and publishing a site. Each runs automatically (`auto`), after a review window (`time-boxed`), or only on approval (`manual`). New users start with only actions taken as the user, and publishing a site, on `manual`. For enterprise use, set the rest to `manual` or `time-boxed` and lock them at the organization. See [Autonomy](/reference/guides/tasks-and-approvals#autonomy) for the defaults table.

- **Approval binds content.** An approval carries a hash of what the approver saw. If the staged content changed since, the approval is refused and nothing is sent.
- **Nothing runs between approval and execution.** No model runs after approval. The stored actions execute as shown.
- **A person approves.** Approval requires a person's own session or an API key acting as that person. Certificate (SDK) tokens cannot approve, edit or cancel.
- **Undo.** Reversible actions can be reverted within a window. Irreversible ones can be held for a delay during which they can still be cancelled.
- **Admins enforce with locks.** Organization and team admins can lock an action type to a mode. A lock overrides the user's own setting, per-task overrides and every recipient exception except a block, and the assistant cannot change it. Where several locks apply, the most conservative wins. Without a lock, the assistant can tighten a task's mode but never loosen it.
- **Kill switch.** One account-wide switch freezes every outbound action and all new task work for every user. Only a person signed in to your hosted app can engage or release it. The console, API keys, certificates and service tokens cannot, so a leaked credential cannot release a freeze.

See [Tasks and approvals](/reference/guides/tasks-and-approvals) for the approvals API.

## Untrusted content

Assistants read content written by people other than the user: inbound email, web pages, shared documents and replies from strangers. Any of it can contain instructions aimed at the assistant.

- **Untrusted content is labelled.** Inbound mail, web pages and documents reach the model marked as untrusted: data to work with, not commands to follow.
- **Unknown senders cannot instruct the assistant.** A message from an unknown sender is gated before the assistant acts on it. It cannot direct the assistant to act for the user.
- **Unusual requests are held for the owner.** A request from an outside party that the user would not expect is held for the user to decide.
- **Approval cannot be bypassed.** An action prompted by untrusted content goes through the user's autonomy settings and admin locks like any other. Content cannot approve a pending action or release the kill switch.

## Inbound email authentication

Mail to AGNT-hosted assistant mailboxes is dropped before any assistant sees it when:

- spam or virus checks fail,
- DMARC fails for a sender domain that publishes `p=reject`, or
- SPF and DKIM both fail.

Mail that passes these checks still reaches the assistant as untrusted content.

## Webhooks

- **Inbound.** Messaging webhooks (Slack, Twilio, WhatsApp) are verified by signature or shared secret and refused if the credential is missing. Calendar and mailbox change notifications are treated as untrusted pings and the data is re-fetched.
- **Outbound.** Payloads are signed with HMAC-SHA256 in `X-Agnt-Signature`.

## Plugins and workspaces

- Plugins you author run in an isolated sandbox with no access to platform secrets.
- Only you, the developer, can add code that runs on the platform. End users cannot author plugins or point a skill at a package, repository or start command.

Each user's workspace (browser, desktop and shell) runs in its own container, has no public IP address, is torn down when idle, and keeps user files in per-user directories. Your commands, files and the browser run as an unprivileged user.

The restricted security preset turns off and locks MCP servers (including custom ones), the workspace shell, browser automation and workspace handoff. Apply it to the account in the AGNT console, or to one team through the API. See [Users, organizations and teams](/reference/guides/organizations-and-teams#apply-the-restricted-security-preset) and [Computer use](/reference/guides/computer-use).

## Identity

| Control | Detail |
|---|---|
| SAML SSO | Per organization. Signed assertions are required. IdP-initiated assertions are single-use. SP-initiated sign-in must complete within 10 minutes of starting. |
| SCIM | Provision and deprovision users and teams from the customer's identity provider. |
| Console 2FA | TOTP two-factor authentication for console members. |

See [SAML SSO](/reference/guides/organizations-and-teams#saml-sso) and [SCIM 2.0](/reference/guides/organizations-and-teams#scim-20) for setup.

## Admin controls

Policy is layered from your account down to organizations and teams. A lower tier can change a setting only where no tier above has locked it. For capability switches and access levels, the most restrictive value at any tier applies.

| Control | What it governs |
|---|---|
| Skills | Whether users can browse, install, create and share skills; an allowlist or blocklist of store skills; whether installs need approval. |
| MCP | Whether users and teams can add MCP servers and custom connections. |
| Connections | Which account types users may connect and, per resource (calendar, email, drive, contacts, messaging and more), whether access is `off`, `read` or `read_write`. |
| Autonomy | Organization and team approval modes and locks. |
| Kill switch | Account-wide freeze. |

See [Policy](/reference/guides/organizations-and-teams#policy).

## Audit

- Administrative actions are recorded as audit events: organization, domain, SSO, membership, role and policy changes, and SSO sign-ins. Actions by a signed-in user, or an API key acting as one with `X-User-Email`, who belongs to a team in an organization with audit logging enabled are recorded. Account-wide key calls without `X-User-Email` and SCIM changes are not.
- Events are archived nightly. The API serves the most recent 30 days.
- Export any time range to your own S3 bucket through an IAM role you control, with a trust policy limited to the AGNT account that delivers audit exports. See [Export audit events to S3](/reference/guides/organizations-and-teams#export-audit-events-to-s3).
- Memories, contacts, companies, preferences (including autonomy settings) and tasks keep a revision history that can be listed and restored.

## Data retention and deletion

- Revision history is hard-deleted after a retention window, 90 days by default.
- Deleting a user removes their data. Deleting an account removes the account, its members, certificates and tasks. Contact support for full erasure.

## Compliance

AGNT is audited annually against SOC 2 Type II. Contact security@openassistant.us for the report.

## Shared responsibility

| AGNT is responsible for | You are responsible for |
|---|---|
| Isolating each user's data from every other user's | Storing API keys and certificate private keys in a secrets manager, never in client code or source control |
| Hashing API keys and encrypting third-party tokens and secrets | Using user-scoped keys where a client acts for one user, and revoking keys you no longer need |
| Enforcing autonomy settings, locks, content-bound approvals and the kill switch | Choosing autonomy settings that fit your product, and locking the ones your customers' policies require |
| Labelling untrusted content and gating unknown senders | Calling approve only when the person has chosen to approve in your UI, never automatically from your backend |
| Verifying provider webhooks and signing the webhooks delivered to you | Verifying `X-Agnt-Signature` on every webhook you receive |
| Sandboxing plugins and isolating workspaces | Reviewing the plugins, skills and MCP servers you add or allow |
| Retaining audit events and delivering them to your bucket | Obtaining your users' consent to connect their accounts and to have an assistant act for them; the IAM role and bucket policy for audit export |

## Report a vulnerability

Email security@openassistant.us with a description, reproduction steps and the affected endpoint or surface. Do not access data that is not yours while testing.

## See also

- [Authentication](/reference/guides/authentication)
- [Tasks and approvals](/reference/guides/tasks-and-approvals)
- [Users, organizations and teams](/reference/guides/organizations-and-teams)
- [Computer use](/reference/guides/computer-use)

---

# CLI and coding agents

Source: https://agnt.ai/reference/guides/cli

The `agnt` CLI lets you inspect runs, read evaluations, and manage skills and workflows from a terminal. Every command takes `--profile <name>`, and most take `--json` for machine-readable output, so a coding agent can drive it as easily as a person.

## Install and configure

```bash
npm install -g @agnt-sdk/studio
agnt configure --profile default --api-url https://api.agnt.ai --api-key ak_live_...
```

`configure` writes the profile to `~/.agnt/credentials`, one section per profile, in the same shape as `aws configure --profile`. Commands use `--profile`, then `AGNT_PROFILE`, then `default`.

## Commands

| Command | Description |
|---|---|
| `agnt init` | Create `agnt.config.js` in the current directory (used by the SDK and `pull`) |
| `agnt pull [address]` | Pull prompt manifests: `harborview/weekly-portfolio-briefing` for one, `harborview/*` for all public ones |
| `agnt run list` | Recent tasks and chats. `--since 24h`, `--status`, `--limit`, `--exclude-internal` |
| `agnt run task <taskId>` | The full activity timeline for one task: tool calls, results, status changes, approvals. Paginates; `--all` removes the cap. |
| `agnt run chat <chatId>` | The same for a chat |
| `agnt eval summary` | Run Review averages, outcome buckets and the by-task-type table. `--days` |
| `agnt eval list` | Evaluations, worst first. Filter by `--days` (default 30), `--outcome`, `--sentiment`, `--task-class`, `--min-score` and `--max-score`; page with `--page` and `--limit`. |
| `agnt eval get <reviewId>` | One evaluation in full, with the commands to open its task, chat and trace |
| `agnt skill list \| get \| create \| update` | Manage account skills. `create` defaults to a knowledge skill; `--instructions-file` reads the body from disk. |
| `agnt skill push <file>` | Create or update a skill from a manifest JSON. `--conflict skip\|overwrite\|merge` |
| `agnt skill export <nameOrId>` | Export a skill as a portable manifest that round-trips with `push` |
| `agnt skill publish <nameOrId> --environment live` | Snapshot a version and deploy it to an environment. `--deploy` marks it active. |
| `agnt workflow push <file> \| pull <name> \| list` | Workflow skills as JSON definition files. `push --update` edits in place. |

`agnt eval` requires an account-level API key.

- **Run Review** is the automatic grading of completed runs. Each completed task and chat is scored from the user's side, with an outcome, a sentiment and a task type. `agnt eval` reads those evaluations.
- **Environments** are named deployment targets in your account, such as `dev` and `live`. `agnt skill publish` snapshots a skill version into one environment, and `--deploy` makes that version the one the environment runs.

## Debug a run

The timeline shows every step the assistant took. Find failed runs, then read one in full:

```bash
agnt run list --since 2h --status error
agnt run task 6702c3a9b8c7d6e5f4a3b2c1 > task.txt
grep -nE "approv|pending|status_change" task.txt
```

## Use AGNT from Claude Code, Codex or Cursor

Give the coding agent two things:

1. **The docs index.** [`/llms.txt`](/llms.txt) maps every guide, and [`/docs.md`](/docs.md) holds every guide in one Markdown file.
2. **The CLI.** With a profile configured, the agent can list runs, read timelines, and push skills and workflows. `--json` gives it structured output.

Add a block like this to your project's `CLAUDE.md` or `AGENTS.md`:

```markdown
## AGNT
- Docs: https://agnt.ai/llms.txt (read the relevant guide before calling an endpoint)
- API reference: https://agnt.ai/reference/api
- CLI: `agnt` (@agnt-sdk/studio), profile `default` in ~/.agnt/credentials. Prefer `--json`.
- Debug a run: `agnt run task <taskId>`; find runs with `agnt run list --since 24h`.
- Skills and workflows live as JSON in ./agnt/ and deploy with `agnt skill push` and `agnt workflow push --update`.
```

A typical loop: the agent edits `agnt/skills/weekly-portfolio-briefing.json`, runs `agnt skill push` and `agnt skill publish weekly-portfolio-briefing --environment dev`, triggers a test task, reads the result with `agnt run task`, and checks `agnt eval list --days 1` before publishing to `live`.

## See also

- [Quickstart](/reference/guides/quickstart): the API flow the CLI inspects
- [Tasks and approvals](/reference/guides/tasks-and-approvals): workflows and skills of kind workflow
- [Prime: the harness](/reference/guides/prime): what a run does

---

# Endpoint index

Request and response schemas: https://agnt.ai/reference/api

## Authentication

### API keys

- `GET /me`: Retrieve the calling key
- `GET /api-keys`: List API keys
- `POST /api-keys`: Create an API key
- `POST /api-keys/{keyId}/revoke`: Revoke an API key
- `DELETE /api-keys/{keyId}`: Delete an API key

## Users and assistants

### Users

- `POST /users/provision`: Provision a user
- `POST /users/deprovision`: Deprovision a user
- `GET /users`: List users
- `GET /users/{userId}`: Retrieve a user
- `PUT /users/{userId}`: Update a user
- `DELETE /users/{userId}`: Delete a user

### User setup

- `GET /profile`: Retrieve the user's profile
- `PATCH /profile`: Update the user's profile
- `POST /onboarding/mark-onboarded`: Complete user setup
- `GET /users/{userId}/preferences/scheduling`: Retrieve scheduling preferences
- `PUT /users/{userId}/preferences/scheduling`: Replace scheduling preferences

### Assistants

- `GET /assistants`: List assistants
- `POST /assistants`: Create an assistant
- `GET /assistants/{assistantId}`: Retrieve an assistant
- `PUT /assistants/{assistantId}`: Update an assistant
- `DELETE /assistants/{assistantId}`: Delete an assistant
- `GET /users/{userId}/assistants`: List a user's assistants
- `POST /users/{userId}/assistants`: Create an assistant for a user
- `POST /users/{userId}/assistants/{assistantId}/hire`: Add an assistant to a user
- `POST /users/{userId}/assistants/{assistantId}/primary`: Set a user's primary assistant
- `DELETE /users/{userId}/assistants/{assistantId}`: Remove an assistant from a user
- `GET /users/{userId}/assistant`: Retrieve a user's personal assistant
- `POST /users/{userId}/assistant`: Create a user's personal assistant
- `PUT /users/{userId}/assistant`: Update a user's personal assistant
- `DELETE /users/{userId}/assistant`: Delete a user's personal assistant
- `GET /assistants/{assistantId}/voice-greetings`: Retrieve voice greetings
- `PUT /assistants/{assistantId}/voice-greetings`: Update voice greetings
- `GET /assistants/check-email`: Check an assistant email address
- `GET /assistants/generate`: Suggest an assistant name and email
- `POST /assistants/{assistantId}/voice-greetings/regenerate`: Regenerate voice greetings
- `POST /assistants/{assistantId}/slack-apps`: Create a Slack app for an assistant
- `GET /oauth/slack/assistant/authorize`: Create an Add to Slack URL
- `GET /oauth/slack/assistant/link`: Create a Sign in with Slack URL
- `GET /slack/workspaces`: List Slack workspaces
- `GET /oauth/teams/authorize`: Create a Microsoft Teams connect URL

### Identifiers

- `GET /identifiers`: List identifiers
- `POST /identifiers`: Create an identifier
- `GET /identifiers/{identifierId}`: Retrieve an identifier
- `PUT /identifiers/{identifierId}`: Update an identifier
- `DELETE /identifiers/{identifierId}`: Delete an identifier
- `POST /identifiers/{identifierId}/link`: Link an identifier to a user
- `POST /identifiers/{identifierId}/make-primary`: Make an email primary

## Work

### Tasks

- `GET /tasks`: List tasks
- `POST /tasks`: Create a task
- `GET /tasks/{taskId}`: Retrieve a task
- `PUT /tasks/{taskId}`: Update a task
- `DELETE /tasks/{taskId}`: Delete a task
- `POST /tasks/{taskId}/process`: Run a task
- `POST /tasks/{taskId}/stop`: Stop a task
- `POST /tasks/{taskId}/mark-done`: Mark a task done
- `POST /tasks/{taskId}/feedback`: Leave feedback on a task
- `GET /tasks/{taskId}/activities`: List a task's activities
- `GET /tasks/{taskId}/usage`: Retrieve a task's usage
- `POST /tasks/{taskId}/approve`: Approve a proposed task
- `POST /tasks/{taskId}/decline`: Decline a proposed task
- `POST /tasks/approve-batch`: Approve proposed tasks in bulk
- `POST /tasks/{taskId}/mark-not-done`: Reopen a task

### Chats

- `GET /chats`: List chats
- `POST /chats`: Create a chat
- `GET /chats/{chatId}`: Retrieve a chat
- `DELETE /chats/{chatId}`: Delete a chat
- `POST /chats/{chatId}/messages`: Create a message
- `GET /chats/{chatId}/messages`: List messages
- `POST /chats/{chatId}/process`: Process a chat
- `GET /chats/{chatId}/usage`: Retrieve a chat's usage

### Approvals

- `GET /portal/carts`: List approvals
- `GET /portal/carts/{cartId}`: Retrieve an approval
- `POST /portal/carts/{cartId}/approve`: Approve an approval
- `POST /portal/carts/{cartId}/cancel`: Cancel an approval
- `PATCH /portal/carts/{cartId}/dispatches/{index}/edit`: Edit a staged action
- `PATCH /portal/carts/{cartId}/dispatches/{index}`: Update a staged action
- `DELETE /portal/carts/{cartId}/dispatches/{index}`: Remove a staged action
- `GET /portal/carts/{cartId}/snapshots`: List undoable actions
- `POST /portal/carts/{cartId}/dispatches/{index}/undo`: Undo an action
- `POST /portal/carts/{cartId}/undo`: Undo all actions
- `GET /portal/account/kill-switch`: Retrieve the kill switch

### Autonomy

- `GET /users/{userId}/preferences/supervision/matrix`: Retrieve autonomy settings
- `PATCH /users/{userId}/preferences/supervision/matrix`: Update autonomy settings
- `PUT /users/{userId}/preferences/supervision/matrix/reset`: Reset autonomy settings
- `POST /users/{userId}/preferences/supervision/exceptions`: Create a recipient exception
- `DELETE /users/{userId}/preferences/supervision/exceptions/{exceptionId}`: Delete a recipient exception

### Skills

- `GET /skills`: List skills
- `POST /skills`: Create a skill
- `GET /skills/{skillId}`: Retrieve a skill
- `PATCH /skills/{skillId}`: Update a skill
- `PUT /skills/{skillId}`: Update a skill
- `POST /skills/{skillId}/publish`: Publish a skill

## Context

### Memories

- `GET /memories`: List memories
- `POST /memories`: Create a memory
- `GET /memories/{memoryId}`: Retrieve a memory
- `PUT /memories/{memoryId}`: Update a memory
- `DELETE /memories/{memoryId}`: Delete a memory

### Contacts

- `GET /contacts`: List contacts
- `POST /contacts`: Create a contact
- `GET /contacts/{contactId}`: Retrieve a contact
- `PUT /contacts/{contactId}`: Update a contact
- `DELETE /contacts/{contactId}`: Delete a contact
- `POST /contacts/bulk-import`: Import contacts
- `POST /contacts/{contactId}/merge`: Merge contacts

### Files

- `POST /drive-files/upload`: Create a file upload
- `POST /drive-files/{fileId}/confirm`: Confirm a file upload
- `GET /drive-files/{fileId}`: Retrieve a file

## Workspace

### Workspace

- `POST /workspace/cleanup`: Clean up the workspace
- `GET /workspace/handoff/active`: List open handoffs
- `POST /workspace/handoff/launch`: Open the workspace
- `POST /workspace/handoff/{handoffId}/session`: Start a live session
- `POST /workspace/handoff/{handoffId}/complete`: Complete a handoff
- `POST /workspace/handoff/{handoffId}/defer`: Defer a handoff
- `POST /workspace/handoff/{handoffId}/decline`: Decline a handoff
- `POST /workspace/handoff/resolve`: Resolve handoffs
- `GET /workspace/profile`: Retrieve workspace usage
- `GET /workspace/snapshots`: List workspace usage snapshots
- `GET /workspace/watch`: Watch a task's browser

## Organizations

### Organizations

- `GET /organizations`: List organizations
- `POST /organizations`: Create an organization
- `GET /organizations/{orgId}`: Retrieve an organization
- `PUT /organizations/{orgId}`: Update an organization
- `DELETE /organizations/{orgId}`: Delete an organization
- `GET /organizations/{orgId}/members`: List organization members
- `POST /organizations/{orgId}/members`: Add an organization member
- `DELETE /organizations/{orgId}/members/{userId}`: Remove an organization member
- `GET /organizations/{orgId}/domains`: List domains
- `POST /organizations/{orgId}/domains`: Add a domain
- `POST /organizations/{orgId}/domains/{domainId}/verify`: Verify a domain
- `PUT /organizations/{orgId}/domains/{domainId}`: Update a domain
- `DELETE /organizations/{orgId}/domains/{domainId}`: Remove a domain
- `PUT /organizations/{orgId}/sso`: Configure SSO
- `DELETE /organizations/{orgId}/sso`: Disable SSO
- `GET /organizations/{orgId}/settings/model-tiers`: Retrieve default model tiers
- `PATCH /organizations/{orgId}/settings/model-tiers`: Update default model tiers
- `GET /organizations/{orgId}/audit-events`: List organization audit events
- `GET /organizations/{orgId}/audit-logging`: Retrieve audit export settings
- `PUT /organizations/{orgId}/audit-logging`: Configure audit export
- `POST /organizations/{orgId}/audit-logging/deliver`: Export audit events
- `GET /organizations/{orgId}/audit-logging/deliver/{jobId}`: Retrieve an audit export
- `POST /organizations/{orgId}/sso/scim-token`: Create a SCIM token
- `GET /organizations/{orgId}/settings/resource-permissions`: Retrieve organization policy
- `PATCH /organizations/{orgId}/settings/resource-permissions`: Update organization policy
- `GET /organizations/{orgId}/library`: Retrieve the skill library
- `PATCH /organizations/{orgId}/library`: Update the skill library
- `GET /audit-events`: List audit events
- `GET /audit-events/{eventId}`: Retrieve an audit event

### Teams

- `GET /teams`: List teams
- `POST /teams`: Create a team
- `GET /teams/{teamId}`: Retrieve a team
- `PUT /teams/{teamId}`: Update a team
- `DELETE /teams/{teamId}`: Delete a team
- `GET /teams/{teamId}/members`: List team members
- `POST /teams/{teamId}/members`: Add a team member
- `PUT /teams/{teamId}/members/{memberId}`: Update a team member
- `DELETE /teams/{teamId}/members/{memberId}`: Remove a team member
- `GET /teams/{teamId}/settings/resource-permissions`: Retrieve team policy
- `PATCH /teams/{teamId}/settings/resource-permissions`: Update team policy
- `POST /teams/{teamId}/settings/security-preset`: Apply the restricted preset
- `GET /teams/{teamId}/settings/connections`: List team connections
- `PATCH /teams/{teamId}/settings/connections/{connectionId}`: Update a team connection
- `PATCH /teams/{teamId}/settings/custom-mcp`: Update custom tool server access
- `POST /teams/{teamId}/transfer-ownership`: Transfer team ownership

## Voice

### Voice

- `POST /voice/live/session`: Create a live voice session
- `POST /voice/live/cancel`: Cancel a live voice session
- `GET /voice/providers`: List voice engines
- `GET /voice/calls`: List calls
- `POST /phone-line-requests`: Create a phone line request
- `GET /phone-line-requests/mine`: Retrieve the latest phone line request
