# agents (/docs/reference/api/agents)

> An agent is an AI agent registered as an account of its own: an identity of kind agent that a person answers for.



An agent is an AI agent registered as an account of its own: an identity of kind `agent` that a person answers for.
Like a service account it holds API keys ([`credentials.create`](/docs/reference/api/credentials#create)) and never
signs in, and roles, groups, and policies apply to it as to anyone. Two things set it apart: its credentials work only
while its sponsor is an active person of the same tenant, and its `boundary` policy caps everything it does, whatever
its roles say and whoever it acts for. People let an agent act on their behalf with
[delegations](/docs/reference/api/delegations). The repository guide is `docs/agents.md`.

## Sponsors and standing [#sponsors-and-standing]

Every agent has a sponsor (`agent.sponsorId`): an active, unexpired person of the agent's tenant, accountable for it.
`create` makes the caller the sponsor when the caller is a person in their own session of the tenant; otherwise name
one with `sponsorId`. An agent's **standing** says whether it may act right now:

* `ok`: it may act.
* `suspended`: the agent is disabled, as `suspend` does.
* `expired`: its `expiresAt` has passed.
* `deleted`: it was deleted.
* `sponsor-missing`: no person of the tenant matches `sponsorId`.
* `sponsor-inactive`: the sponsor is disabled, deleted, or past their own `expiresAt`.

Anything but `ok` refuses every credential of the agent with `UNAUTHENTICATED`: its API keys, its session tokens, and
the delegated sessions in which it acts for people. `credentials.create` refuses new keys meanwhile
(`INVALID_IDENTITY`). Offboarding a sponsor with a successor ([`identities.offboard`](/docs/reference/api/identities#offboard))
hands their agents to the successor, audited as `agent:sponsor-change`; without a successor the agents stay with the
leaver and are refused until an administrator names a new sponsor with `update`.

## Profile and ceiling [#profile-and-ceiling]

The profile fields `create` and `update` accept (in `update`, `null` clears an optional one):

| Field                        | Meaning                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `model`, `provider`          | What the agent runs on. The model is 1 to 128 letters, digits, or `._:/@+-`; the provider 1 to 64 letters, digits, or `._-`, stored in lowercase.                                                                                                                                                                                                                                                                                                                                          |
| `purpose`, `url`             | Shown to people deciding whether to delegate to the agent: up to 1024 characters, and an http(s) URL.                                                                                                                                                                                                                                                                                                                                                                                      |
| `protocols`                  | An informational list such as `mcp` and `a2a`: at most 16 short lowercase names, deduplicated and sorted.                                                                                                                                                                                                                                                                                                                                                                                  |
| `boundary`                   | A policy document that caps everything the agent does, with its own keys and in delegated sessions. Changes apply to live sessions at once.                                                                                                                                                                                                                                                                                                                                                |
| `delegable`                  | `false` refuses new delegations and the use of existing ones until it is turned back on.                                                                                                                                                                                                                                                                                                                                                                                                   |
| `maxDelegatedSessionSeconds` | The longest delegated session the agent may open, 60 to 43200 seconds (3600 when unset).                                                                                                                                                                                                                                                                                                                                                                                                   |
| `tokenAudiences`             | Services outside Better IAM the agent may present a person's delegation to with a [delegation token](/docs/reference/api/delegations#issuetoken): at most 16 http(s) URLs or other absolute URIs (`urn:example:api`), without user info, query, or fragment. In a URL, `*` may start the host (`https://*.example.com`, any subdomain) or appear in the path (`https://api.example.com/v1/*`); a URL without a path matches only the root. Without it the agent gets no delegation tokens. |

Decisions for an agent's own key, and for sessions in which it acts for someone, carry `principal.agentId`,
`principal.agentSponsorId`, and, when set, `principal.agentModel` and `principal.agentProvider`; `principal.kind` is
`agent` for the agent's own key. See [principal keys](/docs/guides/authorization/conditions#principal-keys).

## Who may call what [#who-may-call-what]

Administrators use `iam:agents:create`, `iam:agents:read`, `iam:agents:update`, and `iam:agents:delete`, checked on
`iam/{agentId}` (on the tenant for `create` and `list`). A sponsor manages their own agents from their own signed-in
session of the tenant without any permission: `listMine`, `get`, `standing`, `activity`, `signCard`, `suspend`, and
`resume` of a suspension they made themselves. An agent may call `signCard` for itself with its own unscoped key. When
the caller is the agent's sponsor, these sponsor rules apply even if they also hold the administrative permission. Every person of the tenant may browse delegable agents with `catalog`. A "person's own
session" excludes API keys, role sessions, session tokens, delegated sessions, and impersonation.

| Method                      | What it does                                                                                                                                  | Access     |
| --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | ---------- |
| [`activity`](#activity)     | Returns what an agent did, newest first: the audit events of its own credentials and of the sessions in which it acted for people.            | Credential |
| [`catalog`](#catalog)       | Lists the agents people of the tenant may delegate to, with what a person needs to decide.                                                    | Credential |
| [`create`](#create)         | Registers an AI agent with a sponsor and an optional profile.                                                                                 | Credential |
| [`delete`](#delete)         | Deletes an agent, ending its keys and sessions and revoking every delegation to it.                                                           | Credential |
| [`directory`](#directory)   | Lists the tenant's agents that hold a current attested A2A card, for finding an agent to work with or hand work to.                           | Credential |
| [`get`](#get)               | Returns one agent with its standing, its live API keys, and counts of its delegations.                                                        | Credential |
| [`list`](#list)             | Lists the tenant's agents, newest first, optionally only one sponsor's or those in one standing.                                              | Credential |
| [`listMine`](#listmine)     | Returns the agents you sponsor, newest first, with the same detail as `get`.                                                                  | Credential |
| [`resume`](#resume)         | Lifts an agent's suspension, so its kept API keys work again.                                                                                 | Credential |
| [`signCard`](#signcard)     | Signs an agent's A2A (Agent2Agent) agent card, so other agents can check that it is a registered agent in good standing of your organization. | Credential |
| [`standing`](#standing)     | Tells whether an agent may act right now and, if not, why.                                                                                    | Credential |
| [`suspend`](#suspend)       | Stops an agent at once (the kill switch) and keeps its API keys for `resume`.                                                                 | Credential |
| [`suspendAll`](#suspendall) | The organization-wide emergency stop: suspends every active agent of the tenant at once.                                                      | Credential |
| [`update`](#update)         | Changes an agent's name, description, expiry, attributes, profile, or sponsor.                                                                | Credential |

## activity [#activity]

Returns what an agent did, newest first: the audit events of its own credentials and of the sessions in which it acted for people.

**HTTP:** `POST /api/iam/agents/activity` (requires a credential) · **Browser client:** `client.agents.activity()`

* **Permission:** `iam:agents:read` on the agent, or none for its sponsor in their own session.
* **Audited as:** `iam:agents:read` for administrators; not audited for the sponsor.
* **Errors:** `NOT_FOUND` when the id is not an agent of this tenant; `INVALID_INPUT` for a `limit` outside 1 to 500 or
  a malformed `offset`, `from`, or `to`.

An event belongs to the agent when the agent is its actor (its API keys and session tokens) or when its
`sessionContext.agentId` names the agent (a delegated session, where the actor is the person it acted for). Both
allowed and denied events are included, so a sponsor sees what the agent tried as well as what it did. Page with
`limit` (100 by default) and `offset`, and bound the time with `from` and `to` (epoch milliseconds).

```ts
const recent = await iam.api.agents.activity(sponsorSession, { tenantId, agentId, limit: 50 });
const refused = recent.filter((event) => event.outcome === 'deny');
```

```ts title="Signature"
iam.api.agents.activity(
  credential: CredentialInput,
  input: ActivityQuery & { tenantId: string; agentId: string },
): Promise<AuditEvent[]>
```

## catalog [#catalog]

Lists the agents people of the tenant may delegate to, with what a person needs to decide.

**HTTP:** `POST /api/iam/agents/catalog` (requires a credential) · **Browser client:** `client.agents.catalog()`

* **Permission:** None beyond a person's own session of the tenant.
* **Audited as:** Not audited; it only reads.
* **Errors:** `ACCESS_DENIED` for any other credential, including a session of another tenant.

Only active, delegable agents in good standing (`ok`) are listed, sorted by name. Each entry has `id` and `name`, the
`description`, `purpose`, `model`, `provider`, `url`, `protocols`, and `tokenAudiences` when set, and `sponsorName`, so
a consent screen can show who answers for the agent and which outside services it may carry a delegation to. Keys, the boundary, and sponsor ids are not included. Use it for a "connect an
agent" page that ends in [`delegations.grant`](/docs/reference/api/delegations#grant).

```ts title="Signature"
iam.api.agents.catalog(
  credential: CredentialInput,
  input: { tenantId: string },
): Promise<AgentListing[]>
```

## create [#create]

Registers an AI agent with a sponsor and an optional profile.

**HTTP:** `POST /api/iam/agents/create` (requires a credential) · **Browser client:** `client.agents.create()`

* **Permission:** `iam:agents:create` on the tenant.
* **Audited as:** `iam:agents:create`, plus `agent:create` with the sponsor, model, and provider.
* **Errors:** `INVALID_SPONSOR` when `sponsorId` is not an active, unexpired person of the tenant, or when it is left
  out and the caller is not a person in their own session (an API key, for example); `LIMIT_EXCEEDED` (409) past the
  tenant's `agents` limit; `INVALID_INPUT` for a malformed profile field, a `description` over 512 characters, or an
  `expiresAt` that is not in the next ten years; `INVALID_POLICY` or `INVALID_ACTION` for a malformed `boundary`.

The agent starts `active` with no keys: issue them with `credentials.create` as for a service account, and give it
access with roles and bindings. `expiresAt` (epoch milliseconds) schedules its deactivation. Deleted agents do not
count toward the limit. The result carries the agent's `standing` and a `sponsor` summary (id, name, email, status).

```ts
const agent = await iam.api.agents.create(credential, {
  tenantId,
  name: 'Support triage',
  purpose: 'Labels and routes incoming support tickets',
  model: 'claude-sonnet-5',
  provider: 'anthropic',
  protocols: ['mcp'],
  sponsorId: aliceId,
  boundary: {
    version: 1,
    statements: [{ effect: 'allow', actions: ['tickets:*'], resources: ['ticket/*'] }],
  },
});
const { token } = await iam.api.credentials.create(credential, {
  tenantId,
  identityId: agent.id,
  name: 'production',
});
```

```ts title="Signature"
iam.api.agents.create(
  credential: CredentialInput,
  input: CreateAgentInput,
): Promise<AgentSummary>
```

## delete [#delete]

Deletes an agent, ending its keys and sessions and revoking every delegation to it.

**HTTP:** `POST /api/iam/agents/delete` (requires a credential) · **Browser client:** `client.agents.delete()`

* **Permission:** `iam:agents:delete` on the agent, and a recent sign-in.
* **Audited as:** `iam:agents:delete`, plus `identity:delete` with `kind: 'agent'` and `delegationsRevoked`.
* **Errors:** `RECENT_AUTH_REQUIRED` without a recent sign-in (temporary credentials never have one); `NOT_FOUND` when
  the id is not an agent of this tenant; `CONFLICT` when it is already deleted.

Pending requests and active delegations involving the agent become `revoked`, and their delegated sessions end at once.
Like any deleted identity, the agent leaves a tombstone (status `deleted`) so audit records keep resolving it, and its
bindings, group memberships, and relationships are removed. The result is the tombstone. To stop an agent for a while
and keep its keys, use `suspend`.

```ts title="Signature"
iam.api.agents.delete(
  credential: CredentialInput,
  input: { tenantId: string; agentId: string },
): Promise<PublicIdentity>
```

## directory [#directory]

Lists the tenant's agents that hold a current attested A2A card, for finding an agent to work with or hand work to.

**HTTP:** `POST /api/iam/agents/directory` (requires a credential) · **Browser client:** `client.agents.directory()`

* **Permission:** None beyond a credential of the tenant: a person's session, an agent's or service account's key, or
  a delegated session.
* **Audited as:** Not audited; it only reads.
* **Errors:** `ACCESS_DENIED` for a credential of another tenant.

Every [`signCard`](#signcard) stores the agent's latest signed card as its directory entry. Entries are listed while
their attestation is valid and the agent is in good standing, sorted by name: `{ agentId, name, card, attestation,
expiresAt }`, where `card` is the signed card itself, ready for `verifyAgentCard` from `@better-iam/a2a`. `skill`
keeps agents whose card offers a skill with that id or tag, and `protocol` agents whose attestation lists that
protocol. Suspending or deleting an agent removes it; an expired entry is swept away.

```ts
const [translator] = await iam.api.agents.directory(credential, { tenantId, skill: 'translate' });
```

```ts title="Signature"
iam.api.agents.directory(
  credential: CredentialInput,
  input: { tenantId: string; skill?: string; protocol?: string },
): Promise<AgentDirectoryEntry[]>
```

## get [#get]

Returns one agent with its standing, its live API keys, and counts of its delegations.

**HTTP:** `POST /api/iam/agents/get` (requires a credential) · **Browser client:** `client.agents.get()`

* **Permission:** `iam:agents:read` on the agent, or none for the agent's sponsor in their own session.
* **Audited as:** `iam:agents:read` for administrators; not audited for the sponsor.
* **Errors:** `NOT_FOUND` when the id is not an agent of this tenant.

`keys` lists unexpired API keys by label only (id, name, creation and expiry times, and `lastUsedAt` once used), never
token material. `delegations` counts the `active` and `pending` ones that have not lapsed, and
`liveDelegatedSessions` the unexpired delegated sessions. A deleted agent is still returned, with standing `deleted`.

```ts title="Signature"
iam.api.agents.get(
  credential: CredentialInput,
  input: { tenantId: string; agentId: string },
): Promise<AgentDetail>
```

## list [#list]

Lists the tenant's agents, newest first, optionally only one sponsor's or those in one standing.

**HTTP:** `POST /api/iam/agents/list` (requires a credential) · **Browser client:** `client.agents.list()`

* **Permission:** `iam:agents:read` on the tenant.
* **Audited as:** `iam:agents:read`.

Deleted agents are left out unless `includeDeleted: true`. `standing: 'sponsor-inactive'` finds the agents that
stopped because their sponsor left, the ones to hand to someone else with `update`.

```ts title="Signature"
iam.api.agents.list(
  credential: CredentialInput,
  input: {
    tenantId: string;
    sponsorId?: string;
    standing?: AgentSummary['standing'];
    includeDeleted?: boolean;
  },
): Promise<AgentSummary[]>
```

## listMine [#listmine]

Returns the agents you sponsor, newest first, with the same detail as `get`.

**HTTP:** `POST /api/iam/agents/listMine` (requires a credential) · **Browser client:** `client.agents.listMine()`

* **Permission:** None beyond a person's own session of the tenant.
* **Audited as:** Not audited; it only reads.
* **Errors:** `ACCESS_DENIED` for any other credential, including a session of another tenant.

Deleted agents are left out. It backs a "my agents" page where sponsors see keys and delegations and reach the kill
switch.

```ts title="Signature"
iam.api.agents.listMine(
  credential: CredentialInput,
  input: { tenantId: string },
): Promise<AgentDetail[]>
```

## resume [#resume]

Lifts an agent's suspension, so its kept API keys work again.

**HTTP:** `POST /api/iam/agents/resume` (requires a credential) · **Browser client:** `client.agents.resume()`

* **Permission:** `iam:agents:update` on the agent and a recent sign-in, or none for the sponsor when they made the
  suspension themselves.
* **Audited as:** `agent:resume`; an administrator's call also as `iam:agents:update`.
* **Errors:** `ACCESS_DENIED` when the sponsor tries to lift someone else's suspension; `RECENT_AUTH_REQUIRED` for an
  administrator without a recent sign-in; `CONFLICT` (409) when the agent is not suspended; `INVALID_TRANSITION` (409)
  when it has expired (extend or clear `expiresAt` with `update` first); `NOT_FOUND` for a deleted agent.

The agent returns to `active` with its keys, roles, and delegations as they were. Delegated sessions and session
tokens ended by the suspension do not come back; the agent opens new ones. Its standing stays other than `ok` while its
sponsor is inactive.

```ts title="Signature"
iam.api.agents.resume(
  credential: CredentialInput,
  input: { tenantId: string; agentId: string },
): Promise<AgentSummary>
```

## signCard [#signcard]

Signs an agent's A2A (Agent2Agent) agent card, so other agents can check that it is a registered agent in good standing of your organization.

**HTTP:** `POST /api/iam/agents/signCard` (requires a credential) · **Browser client:** `client.agents.signCard()`

* **Permission:** None for the agent itself with its own unscoped API key (not a key with `scopes`, and not a session
  token, role or delegated session); none for its sponsor in their own session; otherwise `iam:agents:update` on the
  agent.
* **Audited as:** `agent:card-sign`, with the card's `url` and the attestation's expiry; an administrator's call also as
  `iam:agents:update`.
* **Errors:** `FEATURE_DISABLED` (403) without the `a2a` option; `INVALID_INPUT` when the agent has no registered `url`,
  when the card's `url`, an `additionalInterfaces[].url` or a `supportedInterfaces[].url` is not on that URL's origin,
  or when the card is not a JSON object with a `name` of 1 to 256 characters (or is larger than 64 KiB);
  `INVALID_IDENTITY` (409) when the agent is not in good standing; `NOT_FOUND` when the id is not an agent of this
  tenant; `ACCESS_DENIED` for anyone else, including another agent's key or a scoped key of the agent itself.

Pass the card as `card`. IAM drops any `signatures` and any attestation already in the card and sets `provider` to the
tenant's name at the agent's registered origin, whatever the card said. It then adds the extension
`urn:better-iam:a2a:attestation:v1` to `capabilities.extensions`. Its params are `issuer`,
`tenantId`, `organization`, `agentId`, `agentName`, `sponsored: true`, `delegable`, `model`, `provider`, `protocols`,
and `issuedAt`/`expiresAt` (ISO 8601, `a2a.cardLifetimeSeconds` apart, one hour by default). IAM signs the RFC 8785
canonical form of the card (without `signatures`) as a detached JWS: `alg` EdDSA or ES256, the key's `kid`, `typ`
`JOSE`, and `jku` when `a2a.jwksUrl` is set. The result is `{ card, attestation, expiresAt }`. `card` carries one
entry in `signatures`, and `expiresAt` is in epoch milliseconds.

Verifiers check the card against the deployment's public card keys (`iam.a2a.jwks()`, served at `a2a.jwksUrl`) with
`verifyAgentCard` or `discoverAgent` from `@better-iam/a2a`. Suspending, deleting, or un-sponsoring the agent stops new
signatures at once, and signed cards stop verifying when their attestation expires. An agent's A2A server usually
re-signs its own card with `createCardAttestor`.

```ts
const { card } = await iam.api.agents.signCard(
  { token: agentKey },
  { tenantId, agentId, card: { name: 'Triage', url: 'https://triage.acme.test/a2a', skills: [] } },
);
```

```ts title="Signature"
iam.api.agents.signCard(
  credential: CredentialInput,
  input: { tenantId: string; agentId: string; card: Record<string, unknown> },
): Promise<SignedAgentCard>
```

## standing [#standing]

Tells whether an agent may act right now and, if not, why.

**HTTP:** `POST /api/iam/agents/standing` (requires a credential) · **Browser client:** `client.agents.standing()`

* **Permission:** `iam:agents:read` on the agent, or none for its sponsor in their own session.
* **Audited as:** `iam:agents:read` for administrators; not audited for the sponsor.
* **Errors:** `NOT_FOUND` when the id is not an agent of this tenant.

The result is `{ agentId, standing }` with one of the values under [Sponsors and standing](#sponsors-and-standing), a
lighter call than `get` when only the answer matters.

```ts title="Signature"
iam.api.agents.standing(
  credential: CredentialInput,
  input: { tenantId: string; agentId: string },
): Promise<{ agentId: string; standing: AgentStanding }>
```

## suspend [#suspend]

Stops an agent at once (the kill switch) and keeps its API keys for `resume`.

**HTTP:** `POST /api/iam/agents/suspend` (requires a credential) · **Browser client:** `client.agents.suspend()`

* **Permission:** `iam:agents:update` on the agent, or none for its sponsor in their own session.
* **Audited as:** `agent:suspend`, with the `reason` and how many delegated sessions and session tokens ended; an
  administrator's call also as `iam:agents:update`.
* **Errors:** `CONFLICT` (409) when it is already suspended; `NOT_FOUND` for a deleted agent or an id that is not an
  agent of this tenant; `ACCESS_DENIED` for anyone else.

The agent becomes `disabled`, and `agent.suspended` records who suspended it, when, and why (`reason`, up to 512
characters). Its API keys are refused while it is suspended, and its live delegated sessions and session tokens are
deleted now, so an agent working for people stops mid-task. Delegations stay in place for after `resume`. No recent
sign-in is needed, so a sponsor can react immediately.

```ts
await iam.api.agents.suspend(aliceSession, { tenantId, agentId, reason: 'Looping on the wiki' });
```

```ts title="Signature"
iam.api.agents.suspend(
  credential: CredentialInput,
  input: { tenantId: string; agentId: string; reason?: string },
): Promise<AgentSummary>
```

## suspendAll [#suspendall]

The organization-wide emergency stop: suspends every active agent of the tenant at once.

**HTTP:** `POST /api/iam/agents/suspendAll` (requires a credential) · **Browser client:** `client.agents.suspendAll()`

* **Permission:** `iam:agents:update` on the tenant; no recent sign-in, so it works during an incident.
* **Audited as:** `agent:suspend-all` with the `reason`, the filters, and how many agents stopped; `agent:suspend` for
  each agent; and `iam:agents:update`.
* **Errors:** `INVALID_INPUT` without a `reason` (up to 512 characters); `ACCESS_DENIED` without the permission.

Each active agent is suspended exactly as [`suspend`](#suspend) does: its credentials are refused and its live
delegated sessions and session tokens end now. `sponsorId`, `provider`, and `model` narrow the stop to one sponsor's
agents or those running on one provider or model. Agents already suspended are left as they are. The result is
`{ suspended, agentIds }`; agents come back one at a time with [`resume`](#resume).

```ts
await iam.api.agents.suspendAll(admin, { tenantId, reason: 'Prompt injection incident' });
```

```ts title="Signature"
iam.api.agents.suspendAll(
  credential: CredentialInput,
  input: {
    tenantId: string;
    reason: string;
    sponsorId?: string;
    provider?: string;
    model?: string;
  },
): Promise<{ suspended: number; agentIds: string[] }>
```

## update [#update]

Changes an agent's name, description, expiry, attributes, profile, or sponsor.

**HTTP:** `POST /api/iam/agents/update` (requires a credential) · **Browser client:** `client.agents.update()`

* **Permission:** `iam:agents:update` on the agent.
* **Audited as:** `iam:agents:update`, plus `agent:sponsor-change` (with `from` and `to`) when the sponsor changes.
* **Errors:** `INVALID_SPONSOR` when the new `sponsorId` is not an active person of the tenant; `NOT_FOUND` for a
  deleted agent or an id that is not an agent of this tenant; `INVALID_INPUT`, `INVALID_POLICY`, or `INVALID_ACTION`
  as for `create`.

Fields you leave out keep their values; `null` clears `description`, `expiresAt`, and the optional profile fields, and
`attributes` replaces the whole set. A new `boundary` and `delegable: false` apply to live sessions at once, because
both are read on every decision. Naming a new sponsor is how you bring back an agent whose sponsor left.

```ts
// The sponsor left without a successor: hand the agent to someone else.
await iam.api.agents.update(credential, { tenantId, agentId, sponsorId: bobId });
```

```ts title="Signature"
iam.api.agents.update(
  credential: CredentialInput,
  input: UpdateAgentInput,
): Promise<AgentSummary>
```
