# Agents API reference

> The agent operations of the Fax for Agents API. Register an agent identity, mint its bounded key, disable it and read its audit trail.

Page: https://faxforagents.com/docs/api/reference/agents/
Last changed: 2026-11-13

Updated Nov 13, 2026

6 operations of the Fax for Agents API, from the OpenAPI document version 1.20.0, snapshot of 2026-11-13. The base URL is `https://api.faxforagents.com/v1`. This is one group of the [full reference](https://faxforagents.com/docs/api/reference/).

- Setting enabled to false on an agent revokes every key of that agent.
- An agent key is never issued approval:decide, key:admin, policy:admin or agent:admin.
- The purpose of an agent is shown to people and is never evaluated by the server.

## Operations

- `GET /v1/agents` List agent identities
- `POST /v1/agents` Register an agent identity
- `GET /v1/agents/{agent_id}` Read an agent with its keys and budgets
- `PATCH /v1/agents/{agent_id}` Change an agent; disabling revokes its keys
- `GET /v1/agents/{agent_id}/activity` The agent's audit trail
- `POST /v1/agents/{agent_id}/keys` Mint a key for the agent (bounded scopes, secret shown once)

## What is an agent identity in the API?

An agent identity is a named principal inside your organization. It has a name, a purpose written for people, a contact address, a list of policies and a status. A fax sent with one of its keys is recorded against the agent. The audit trail then says which agent acted, not only which organization.

Registering an agent does not let it send anything. `POST /v1/agents` writes the identity and answers with its id. The agent can call the API only after a key is minted for it. The read of one agent returns the identity with its keys and its budgets in one answer, so a dashboard needs one call per agent.

## How does an agent get its key?

`POST /v1/agents/{agent_id}/keys` mints a key that belongs to the agent. The body can name a label, an environment, a number of days until it expires and a list of scopes. Leave the scopes out and the key is given the default agent set. The answer carries the secret once. Later reads show the prefix, the scopes and the dates, never the secret.

The scopes are bounded. The schema of the request says an agent key is never issued `approval:decide`, `key:admin`, `policy:admin` or `agent:admin`. A request that asks for one of them is refused. That boundary is why an agent can ask a person to approve a send and cannot approve its own request. The [agents guide](https://faxforagents.com/docs/agents/) lists the full default set and the scopes that are never issued.

## What happens when an agent is disabled?

`PATCH /v1/agents/{agent_id}` changes the name, the purpose, the contact address and the policy list. It also takes `enabled`. Sending `false` disables the agent and revokes each of its keys in the same call, so none of its keys outlives the identity.

Disabling is the fast way to stop one agent without touching the others. Keys of other agents and keys of the organization keep working.

## Where do I see what an agent did?

`GET /v1/agents/{agent_id}/activity` returns the audit rows of one agent, a page at a time. It takes `cursor` and `limit`, and each page carries `next_cursor` while more rows remain. The rows hold metadata about each action. They never hold the content of a document.

## Which scopes do these calls need?

Reading the list, one agent or its activity needs `agent:read`. Registering an agent, changing it and minting a key for it need `agent:admin`. Each operation below states its own scope on its credential line, read from the OpenAPI document.

Agent keys cannot run the admin calls of this group. A person does that from a signed-in session, or a key of the organization that holds `agent:admin` does it from a script. The list call returns every agent of the organization in the environment of the credential.

## How do agents relate to policies and budgets?

An agent names its policies in `policy_ids`. A [delegated policy](https://faxforagents.com/docs/policies/) says where the agent may send and how much, and the server evaluates it on each send. A budget is a spending cap. The read of an agent returns its budgets. The cap itself is created in the [billing group](https://faxforagents.com/docs/api/reference/billing/) with the agent's id.

The three together bound one agent. Its scopes say which calls it may make. The policy says which sends are allowed without a person. The budget says how much it may spend.

## Each operation in detail

### GET `/v1/agents`

List agent identities

Credential: an API key or a signed-in session. Scope: `agent:read`.

#### Responses

GET /v1/agents responses

| Status | Meaning |
| --- | --- |
| `200` | Successful Response |
| `401` | No credential, or one that was not accepted: invalid_token for an API key, unauthenticated where a signed-in session is needed. error.details.reason says why a key was refused. |
| `403` | The credential is valid and is not allowed to do this. scope_missing puts the scope in error.details.scope. Other codes are agent_forbidden and approval_required. A session can also meet csrf_invalid and reauth_required. |
| `429` | rate_limited. Retry-After and the RateLimit-* headers say when to try again. |

#### Response fields

GET /v1/agents response fields

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `items` | array of object Agent | yes |  |

#### Example

```
`curl https://api.faxforagents.com/v1/agents \ -H "Authorization: Bearer ffa_test_..."`
```

### POST `/v1/agents`

Register an agent identity

Credential: an API key or a signed-in session. Scope: `agent:admin`.

#### Parameters

POST /v1/agents parameters

| Name | In | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `Idempotency-Key` | header | string | no | Replay the first successful response for the same request within 24 hours. Reusing the idempotency header with a different request returns 409 idempotency_key_conflict. Responses include Idempotent-Replayed: true on replay. |

#### Request body application/json, required

POST /v1/agents request body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `contact_email` | string or null | no | Where decided approvals are mailed. |
| `name` | string | yes |  |
| `policy_ids` | array of string | no |  |
| `purpose` | string or null | no | Shown to humans, never evaluated. |

#### Responses

POST /v1/agents responses

| Status | Meaning |
| --- | --- |
| `201` | Successful Response |
| `401` | No credential, or one that was not accepted: invalid_token for an API key, unauthenticated where a signed-in session is needed. error.details.reason says why a key was refused. |
| `403` | The credential is valid and is not allowed to do this. scope_missing puts the scope in error.details.scope. Other codes are agent_forbidden and approval_required. A session can also meet csrf_invalid and reauth_required. |
| `404` | not_found: no such resource for this credential. A resource of another organization answers the same. |
| `409` | The resource is in a state that refuses the call, or the Idempotency-Key was used with a different request (idempotency_key_conflict). error.code names the state. |
| `422` | A field failed validation: invalid_request, with one entry per field in error.details, or a code that names the rule. |
| `429` | rate_limited. Retry-After and the RateLimit-* headers say when to try again. |

#### Response fields

POST /v1/agents response fields

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `budget_id` | string or null | yes |  |
| `budgets` | array of object | yes |  |
| `contact_email` | string or null | yes |  |
| `created_at` | string | yes |  |
| `created_by` | string | yes |  |
| `disabled_at` | string or null | yes |  |
| `enabled` | boolean | yes |  |
| `id` | string | yes |  |
| `keys` | array of object ApiKey | yes |  |
| `name` | string | yes |  |
| `policy_ids` | array of string | yes |  |
| `project_id` | string or null | yes |  |
| `purpose` | string or null | yes |  |
| `status` | string | yes |  |
| `updated_at` | string | yes |  |

#### Example

```
`curl https://api.faxforagents.com/v1/agents \ -X POST \ -H "Authorization: Bearer ffa_test_..." \ -H "Content-Type: application/json" \ -d '{"name":"example"}'`
```

### GET `/v1/agents/{agent_id}`

Read an agent with its keys and budgets

Credential: an API key or a signed-in session. Scope: `agent:read`.

#### Parameters

GET /v1/agents/{agent_id} parameters

| Name | In | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `agent_id` | path | string | yes |  |

#### Responses

GET /v1/agents/{agent_id} responses

| Status | Meaning |
| --- | --- |
| `200` | Successful Response |
| `401` | No credential, or one that was not accepted: invalid_token for an API key, unauthenticated where a signed-in session is needed. error.details.reason says why a key was refused. |
| `403` | The credential is valid and is not allowed to do this. scope_missing puts the scope in error.details.scope. Other codes are agent_forbidden and approval_required. A session can also meet csrf_invalid and reauth_required. |
| `404` | not_found: no such resource for this credential. A resource of another organization answers the same. |
| `422` | A field failed validation: invalid_request, with one entry per field in error.details, or a code that names the rule. |
| `429` | rate_limited. Retry-After and the RateLimit-* headers say when to try again. |

#### Response fields

GET /v1/agents/{agent_id} response fields

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `budget_id` | string or null | yes |  |
| `budgets` | array of object | yes |  |
| `contact_email` | string or null | yes |  |
| `created_at` | string | yes |  |
| `created_by` | string | yes |  |
| `disabled_at` | string or null | yes |  |
| `enabled` | boolean | yes |  |
| `id` | string | yes |  |
| `keys` | array of object ApiKey | yes |  |
| `name` | string | yes |  |
| `policy_ids` | array of string | yes |  |
| `project_id` | string or null | yes |  |
| `purpose` | string or null | yes |  |
| `status` | string | yes |  |
| `updated_at` | string | yes |  |

#### Example

```
`curl https://api.faxforagents.com/v1/agents/AGENT_ID \ -H "Authorization: Bearer ffa_test_..."`
```

### PATCH `/v1/agents/{agent_id}`

Change an agent; disabling revokes its keys

Credential: an API key or a signed-in session. Scope: `agent:admin`.

#### Parameters

PATCH /v1/agents/{agent_id} parameters

| Name | In | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `agent_id` | path | string | yes |  |
| `Idempotency-Key` | header | string | no | Replay the first successful response for the same request within 24 hours. Reusing the idempotency header with a different request returns 409 idempotency_key_conflict. Responses include Idempotent-Replayed: true on replay. |

#### Request body application/json, required

PATCH /v1/agents/{agent_id} request body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `contact_email` | string or null | no |  |
| `enabled` | boolean or null | no | false revokes every key of the agent. |
| `name` | string or null | no |  |
| `policy_ids` | array of string or null | no |  |
| `purpose` | string or null | no |  |

#### Responses

PATCH /v1/agents/{agent_id} responses

| Status | Meaning |
| --- | --- |
| `200` | Successful Response |
| `401` | No credential, or one that was not accepted: invalid_token for an API key, unauthenticated where a signed-in session is needed. error.details.reason says why a key was refused. |
| `403` | The credential is valid and is not allowed to do this. scope_missing puts the scope in error.details.scope. Other codes are agent_forbidden and approval_required. A session can also meet csrf_invalid and reauth_required. |
| `404` | not_found: no such resource for this credential. A resource of another organization answers the same. |
| `409` | The resource is in a state that refuses the call, or the Idempotency-Key was used with a different request (idempotency_key_conflict). error.code names the state. |
| `422` | A field failed validation: invalid_request, with one entry per field in error.details, or a code that names the rule. |
| `429` | rate_limited. Retry-After and the RateLimit-* headers say when to try again. |

#### Response fields

PATCH /v1/agents/{agent_id} response fields

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `budget_id` | string or null | yes |  |
| `budgets` | array of object | yes |  |
| `contact_email` | string or null | yes |  |
| `created_at` | string | yes |  |
| `created_by` | string | yes |  |
| `disabled_at` | string or null | yes |  |
| `enabled` | boolean | yes |  |
| `id` | string | yes |  |
| `keys` | array of object ApiKey | yes |  |
| `name` | string | yes |  |
| `policy_ids` | array of string | yes |  |
| `project_id` | string or null | yes |  |
| `purpose` | string or null | yes |  |
| `status` | string | yes |  |
| `updated_at` | string | yes |  |

#### Example

```
`curl https://api.faxforagents.com/v1/agents/AGENT_ID \ -X PATCH \ -H "Authorization: Bearer ffa_test_..." \ -H "Content-Type: application/json" \ -d '{}'`
```

### GET `/v1/agents/{agent_id}/activity`

The agent's audit trail

Credential: an API key or a signed-in session. Scope: `agent:read`.

#### Parameters

GET /v1/agents/{agent_id}/activity parameters

| Name | In | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `agent_id` | path | string | yes |  |
| `cursor` | query | string or null | no |  |
| `limit` | query | integer | no | Default `50` . |

#### Responses

GET /v1/agents/{agent_id}/activity responses

| Status | Meaning |
| --- | --- |
| `200` | Successful Response |
| `401` | No credential, or one that was not accepted: invalid_token for an API key, unauthenticated where a signed-in session is needed. error.details.reason says why a key was refused. |
| `403` | The credential is valid and is not allowed to do this. scope_missing puts the scope in error.details.scope. Other codes are agent_forbidden and approval_required. A session can also meet csrf_invalid and reauth_required. |
| `404` | not_found: no such resource for this credential. A resource of another organization answers the same. |
| `422` | A field failed validation: invalid_request, with one entry per field in error.details, or a code that names the rule. |
| `429` | rate_limited. Retry-After and the RateLimit-* headers say when to try again. |

#### Response fields

GET /v1/agents/{agent_id}/activity response fields

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `items` | array of object AgentActivity | yes |  |
| `next_cursor` | string or null | no |  |

#### Example

```
`curl https://api.faxforagents.com/v1/agents/AGENT_ID/activity \ -H "Authorization: Bearer ffa_test_..."`
```

### POST `/v1/agents/{agent_id}/keys`

Mint a key for the agent (bounded scopes, secret shown once)

Credential: an API key or a signed-in session. Scope: `agent:admin`.

#### Parameters

POST /v1/agents/{agent_id}/keys parameters

| Name | In | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `agent_id` | path | string | yes |  |
| `Idempotency-Key` | header | string | no | Replay the first successful response for the same request within 24 hours. Reusing the idempotency header with a different request returns 409 idempotency_key_conflict. Responses include Idempotent-Replayed: true on replay. |

#### Request body application/json, required

POST /v1/agents/{agent_id}/keys request body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `environment` | string (live, test) | no |  |
| `expires_in_days` | integer or null | no |  |
| `label` | string | no |  |
| `scopes` | array of string or null | no | Omit for the default agent set. Never approval:decide, key:admin, policy:admin, agent:admin. |

#### Responses

POST /v1/agents/{agent_id}/keys responses

| Status | Meaning |
| --- | --- |
| `201` | Successful Response |
| `401` | No credential, or one that was not accepted: invalid_token for an API key, unauthenticated where a signed-in session is needed. error.details.reason says why a key was refused. |
| `403` | The credential is valid and is not allowed to do this. scope_missing puts the scope in error.details.scope. Other codes are agent_forbidden and approval_required. A session can also meet csrf_invalid and reauth_required. |
| `404` | not_found: no such resource for this credential. A resource of another organization answers the same. |
| `409` | The resource is in a state that refuses the call, or the Idempotency-Key was used with a different request (idempotency_key_conflict). error.code names the state. |
| `422` | A field failed validation: invalid_request, with one entry per field in error.details, or a code that names the rule. |
| `429` | rate_limited. Retry-After and the RateLimit-* headers say when to try again. |

#### Response fields

POST /v1/agents/{agent_id}/keys response fields

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `agent_id` | string or null | yes |  |
| `connected_app` | boolean | no | The key belongs to a connected app used over stdio, such as the Gemini CLI extension. The organization's auto-send setting (Settings, Connected apps) binds it like an OAuth connection. |
| `created_at` | string | yes |  |
| `environment` | string (live, test) | yes |  |
| `expires_at` | string or null | yes |  |
| `id` | string | yes |  |
| `label` | string | yes |  |
| `last_used_at` | string or null | yes |  |
| `prefix` | string | yes |  |
| `project_id` | string or null | yes |  |
| `revoked_at` | string or null | yes |  |
| `rotated_from_id` | string or null | yes |  |
| `scopes` | array of string | yes |  |
| `secret` | string | yes | Shown once. Store it now; it cannot be read back. |

#### Example

```
`curl https://api.faxforagents.com/v1/agents/AGENT_ID/keys \ -X POST \ -H "Authorization: Bearer ffa_test_..." \ -H "Content-Type: application/json" \ -d '{}'`
```
