# API keys

Give each agent a key that can do only its job.

A key is made with what it may do: a preset, or actions named. It can also be limited to some sandboxes or templates, to a last day, and to the addresses it may be used from. A key keeps what it was made with. Only its addresses can be changed afterwards.

## Make a key

`POST /v1/api-keys`

```shell
curl -X POST https://agental.io/v1/api-keys \
  -H "Authorization: Bearer $AGENTAL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"ci-github","preset":"ci","templateIds":["tpl_coding_agent"],"expiresAt":"2026-11-15T00:00:00Z","allowFrom":["203.0.113.0/24","2001:db8::/48"]}'
```

| Field | What it does |
| --- | --- |
| `name` | Required. Unique in the account. |
| `preset` | One of the four sets below. Send this or `actions`, not both: neither is `400`. A key that can do everything is asked for with `"preset": "admin"`. |
| `actions` | The actions it may do, named. |
| `sandboxIds`, `templateIds` | The sandboxes it reaches, and the templates it reaches with every sandbox on them. Left out, all of the account's. |
| `expiresAt` | When it stops working. Left out, never. |
| `allowFrom` | IPv4 and IPv6 addresses and ranges it may be used from, 100 at most. Left out, anywhere. |

The answer has the key's `secret`. It is shown this once, and only its hash is kept.

## Presets

| Preset | Actions |
| --- | --- |
| `read-only` | Every read but secrets and mail: `sandbox.read`, `file.read`, `snapshot.read`, `template.read`, `account.read`. |
| `full-sandbox` | Work in sandboxes that are there: `sandbox.read`, `sandbox.auto-stop`, `exec`, `file.read`, `file.write`, `agent.prompt`, `ssh`, `desktop`, `host`, `snapshot.read`, `snapshot.write`, `sandbox.stop`. It makes and deletes none, and touches no outbound rule, secret or account setting. |
| `ci` | `full-sandbox`, and `sandbox.create`, `sandbox.fork`, `sandbox.resume` and `sandbox.delete-own`. |
| `admin` | `*`: everything a key can do. |

A preset is written out into its actions when the key is made. An action added to agental later is not given to keys made before it.

## Actions

| Action | What it allows |
| --- | --- |
| `sandbox.read` | List and read sandboxes, their operations, stops, usage, metrics, template and outbound rule, and the sizes. |
| `sandbox.create` | Make sandboxes, and reconcile creates whose outcome was not known. |
| `sandbox.fork` | Fork a sandbox. |
| `sandbox.update` | Rename a sandbox, share it, put it on another template, take its time limit away. |
| `sandbox.auto-stop` | Read and move when a running sandbox is to be stopped. |
| `sandbox.stop` | Stop and archive. |
| `sandbox.resume` | Start, resume and recover. |
| `sandbox.delete` | Delete any sandbox it reaches. |
| `sandbox.delete-own` | Delete only the sandboxes this key made. |
| `exec` | Run commands. |
| `file.read`, `file.write` | Read and write a sandbox's files. Artifacts are read. |
| `agent.prompt` | Prompt the agent in a sandbox, steer and interrupt it, read its conversations and events. |
| `ssh` | SSH on and off, and the keys let in. |
| `desktop` | The desktop and the browser: turn them on and off, and drive them. |
| `host` | Public addresses for a sandbox's ports. |
| `network.write` | Change the outbound rule of a sandbox or a template. |
| `secret.read`, `secret.write` | Read, set and delete the secret variables and files of sandboxes and templates. |
| `email.read`, `email.send`, `email.manage` | Read a sandbox's mail; send it; turn it on and off and delete messages. |
| `snapshot.read`, `snapshot.write` | List and read snapshots; take, delete and restore them. |
| `template.read`, `template.write` | Templates, other than their secrets and the change of their outbound rule. Applying counts as a change. |
| `key.manage` | Everything under `/v1/api-keys`. |
| `account.read` | The account: who it is, usage, limits, bursts, billing, webhooks, telemetry, data retention, metrics, repositories, invites, organizations, its mail. |
| `account.admin` | Changes to all of those, billing checkout and portal, inviting, and the account's export. |
| `members.read`, `members.admin` | A team's members and invites: list them; invite, revoke invites, change roles and remove members. A key with `account.read` or `account.admin` may too. |
| `*` | Everything a key can do. |

- `network.write` and `secret.read` are actions of their own, so that an agent's key cannot lift its own outbound block or read its template's secrets.
- `account.read` and `account.admin` are about the account as a whole. A key limited to some sandboxes that holds `account.read` still reads the whole account's usage and metrics, which name every sandbox: leave it out of a key you limit.
- Whatever its actions, no key reaches what is for whoever runs the service, nor what only a person signed in may do: allowing the agental command to sign in, and closing, reopening or erasing the account.

## Keys in a team

A key acts in the organization it was made in, for good: your own, or the team you were acting in when you made it. What it makes is that organization's, and it reaches nothing of any other. `GET /v1/me` says which in `activeOrganizationId`.

- A team's owners and admins see and revoke every key of the team, with `createdBy` saying who made each. A member sees and revokes their own.
- What a key may do in a team is held by its actions and by its person's role there, read on each request: a member's key with `members.admin` still removes no one but its person.
- When someone is removed from a team, the keys they made in it go on working, as a member's, until they are revoked. Removing them with `?revokeKeys=true` revokes those keys in the same request, and the console's Remove offers it.
- Deleting a team and accepting an invite are for a person signed in. A key cannot.

```bash
agental org
agental org use Acme
agental members
agental members remove cy@example.com --revoke-keys
agental invite dee@example.com --role admin
```

`agental org use` signs the command in again, in the browser, for a key made in that organization, and deletes the key it had.

## Limit it to some sandboxes or templates

With `sandboxIds`, `templateIds` or both, a key reaches the sandboxes listed, the templates listed with every sandbox on them, and always the sandboxes and templates it made itself. Everything else of the account answers it `404 not_found`, word for word as another account's would, and lists leave it out: sandboxes, snapshots, templates and ports.

- A key limited by `sandboxIds` alone is not limited in templates.
- A sandbox that a key limited to templates makes without naming one goes on the `default` template when that is on its list, otherwise on the one template on its list. With several on its list and not `default`, it has to send `templateId`, and is told `400 template_required`.
- For a key used inside a sandbox, list that sandbox: it is the limit that holds wherever the sandbox runs.

## Limit where it may be used from

`allowFrom` takes addresses, `198.51.100.7`, and ranges, `203.0.113.0/24` or `2001:db8::/48`. A request from anywhere else is refused with `403 key_address_forbidden`, and the message says which address agental saw. An IPv4 address written the IPv6 way, `::ffff:203.0.113.7`, is that IPv4 address.

`PUT /v1/api-keys/{id}/allow-from`

```shell
curl -X PUT https://agental.io/v1/api-keys/key_38b536dc2eabdb52/allow-from \
  -H "Authorization: Bearer $AGENTAL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"allowFrom":["203.0.113.0/24"]}'
```

It replaces the list, from the key's next request. An empty list is anywhere. It is the one change a key takes.

While your account has two-factor sign-in on, a list that lets the key be used from more places, with an address outside the old list or no list at all, takes a `code` when a person signed in sends it, as a new key does. A list that only narrows it needs none.

> **Warning:** A sandbox reaches agental from its server's shared public IPv4 address, and that changes when the sandbox moves to another server. Address limits are for keys used outside agental. Limit a key used inside a sandbox with `sandboxIds`.

> **Warning:** On a network with IPv6, agental sees your IPv6 address, not your IPv4 one. A list of IPv4 addresses alone will refuse you there: add your IPv6 range too.

## A key makes only weaker keys

A key with `key.manage` may make keys, and may change their addresses, but never past itself:

- no action it was not made with;
- no sandbox or template it does not reach, and a list whenever it has one;
- no later last day, and one whenever it has one;
- addresses inside its own, whenever it has some. That holds for a key changing its own list too: it can narrow it, never widen it.

Anything stronger is refused with `403 key_exceeds_caller`, and the message says what.

## With two-factor sign-in on

A key outlives the session that made it, so while your account has [two-factor sign-in](https://agental.io/docs/two-factor) on, a key made in the console asks for a code: the six digits your authenticator app shows now, or a backup code. Signed in to the site, the call sends it as `code`. Without one it is refused with `400 code_required`, a wrong one with `400 invalid_code`, and after five wrong codes in a quarter of an hour that session waits (`429`). A key made with a key needs no code: that key was made behind one.

## Read your keys

`GET /v1/api-keys`

```shell
curl -X GET https://agental.io/v1/api-keys \
  -H "Authorization: Bearer $AGENTAL_API_KEY"
```

Each key says what it may do and where, `lastUsedAt` and `lastUsedFrom`: when it was last used, and from which address. Those are written once a minute at most, so a key in use shows the minute. `GET /v1/api-keys/{id}` reads one, and `DELETE` deletes it: whatever holds it is refused from its next request, and the sandboxes it made stay yours.

## When a key is refused

A request with a key is checked in this order, and stops at the first that fails:

|  | Answer | Why |
| --- | --- | --- |
| 1 | `401 unauthorized` | There is no such key, or it was deleted. |
| 2 | `401 key_expired` | Its last day has passed. |
| 3 | `403 key_address_forbidden` | It came from an address the key may not be used from. The message names it. |
| 4 | `403 key_action_forbidden` | The key was not made with the action the call takes. The message names it. A route no action covers is refused to every key. |
| 5 | `404 not_found` | A sandbox, template, snapshot or port the key does not reach. |

---

Every page of these docs as Markdown: [llms.txt](https://agental.io/llms.txt)
