> ## Documentation Index
> Fetch the complete documentation index at: https://docs.boat.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# API Keys

> Scoped, expiring Box API keys. Least privilege for production.

A Box API key authenticates servers, CI, and the in-box CLI. Every **new** key expires and carries an explicit scope. The bearer header does not change — scope lives on the key, enforced at the Box API boundary.

<Warning>
  Treat API keys as secrets. Store them as `BOX_API_KEY`. Do not commit them, print them in logs, or bake them into images.
</Warning>

Existing keys stay account-wide until you reissue them. Reissue anything that still has no expiry.

## Scope

A request is granted only when **both** are true:

1. The action is in the key's action set.
2. The target box is in the key's box set, or in one of its environments.

Default deny. Unknown routes are refused for scoped keys.

Creating a Box from a named snapshot requires `box.create` for the destination and `snapshot.read` access to the source. Replacing a named snapshot requires access to both its existing source and the Box being saved. Selecting a destination environment does not grant access to snapshots from other environments.

### Actions

| Action                                   | What it unlocks                                      |
| ---------------------------------------- | ---------------------------------------------------- |
| `box.create`                             | Create a box                                         |
| `box.read`                               | List and inspect boxes                               |
| `box.update`                             | Rename, recover, and other box writes                |
| `box.stop`                               | Stop                                                 |
| `box.resume`                             | Resume                                               |
| `box.fork`                               | Fork                                                 |
| `box.delete`                             | Delete                                               |
| `agent.prompt`                           | Prompt the box agent                                 |
| `exec`                                   | Run a command                                        |
| `file.read` / `file.write`               | Read or write files                                  |
| `ssh`                                    | SSH, scp, and port forward                           |
| `desktop`                                | Desktop stream                                       |
| `host`                                   | Host a port                                          |
| `snapshot.read` / `snapshot.write`       | Snapshots                                            |
| `environment.read` / `environment.write` | Environments                                         |
| `account.read`                           | `/me`, limits, list keys, organization discovery     |
| `account.admin`                          | Billing, teams, webhooks, identities, account writes |
| `*`                                      | Admin wildcard                                       |

### Presets

| Preset      | Use                                                                    |
| ----------- | ---------------------------------------------------------------------- |
| `read-only` | Inspect boxes, files, snapshots, environments                          |
| `full-box`  | Operate a box without create / resume / fork / delete / account writes |
| `ci`        | Create, run, snapshot. No account admin                                |
| `admin`     | `*`                                                                    |

### In-box key

The credential written into a box is scoped to **that box**. It can prompt, exec, read and write files, SSH, desktop, host, and snapshot itself. It cannot create, resume, fork, or delete boxes, and it cannot write environments or administer the account. A compromised box is one box.

## Create a key

Creating a key uses `POST /api/box/v1/api-keys/scoped` and requires a browser session (`box login` with no key, or the dashboard) **or** an already-admin token. During a credential rollout the server may temporarily disable creation; `GET /api/box/v1/api-keys` reports `catalog.scopedCreationEnabled`, and the create endpoint returns a typed 503 while it is off.

```bash theme={null}
box api-key create my-project --ttl 90d --preset ci --box bx_123
box api-key create readonly --ttl 30d --preset read-only
box api-key create admin --ttl 7d --preset admin
box api-key create custom --ttl 24h --actions box.read,exec,file.read
```

`--ttl` max is 365 days. `--box` and `--env` are repeatable. `--actions` and `--preset` are mutually exclusive. Omit both and the key is admin-scoped with a 90-day TTL.

The secret is shown once. In scripts:

```bash theme={null}
BOX_API_KEY="$(box api-key create my-project --preset ci --ttl 90d --json | jq -r '.secret')"
```

Or the dashboard **API Keys** tab: expiry presets, action presets, a raw action picker, and box / environment selectors.

<CodeGroup>
  ```bash CLI theme={null}
  box login --key-stdin --json <<< "$BOX_API_KEY"
  ```

  ```bash curl theme={null}
  curl -sS "$BOX_API_BASE/me" \
    -H "Authorization: Bearer $BOX_API_KEY"
  ```

  ```ts TypeScript theme={null}
  import { BoxApi, Configuration } from "@asciidev/box-sdk";

  const box = new BoxApi(new Configuration({
    basePath: "https://ascii.dev/api/box/v1",
    accessToken: process.env.BOX_API_KEY!,
  }));

  const me = await box.me();
  console.log(me.user.login);
  ```

  ```python Python theme={null}
  import os
  from ascii_box_sdk import ApiClient, Configuration
  from ascii_box_sdk.api.box_api import BoxApi

  config = Configuration(host="https://ascii.dev/api/box/v1", access_token=os.environ["BOX_API_KEY"])
  with ApiClient(config) as client:
      box = BoxApi(client)
      me = box.me()
      print(me.user.login)
  ```
</CodeGroup>

Rotate and revoke stay session-gated. Approving an agent claim also requires a browser session. API keys, including admin and legacy keys, cannot approve agent claims.

## List

Listing is the one key operation available on every surface. Secrets are never returned. Each row includes the id, name, prefix, last four characters, scope, expiry, last used, 30-day request total, and how many Boxes and Agents that key created that still exist. Expired and expiring keys are labeled. Restricted API keys receive an empty `apiKeys` list; the account-wide inventory is returned only to a browser/CLI session or an unrestricted account key.

```bash theme={null}
box api-key list
box api-key list --all    # include per-box machine keys
```

```ts TypeScript theme={null}
const keys = await box.apiKeys();
for (const key of keys.apiKeys) {
  console.log(key.id, key.name, key.usage.requests, key.resources.total);
}
```

```python Python theme={null}
keys = box.api_keys()
for key in keys.api_keys:
    print(key.id, key.name, key.usage.requests, key.resources.total)
```

## See usage for one key

`box api-key usage <id>` prints the same 30-day request total and live resource count. Add `--verbose` for the Boxes/Agents split and the created resource list. `GET /api-keys/{id}/usage` is the matching API.

Usage still works after you revoke the key, as long as you still have the id. Revoking stops the secret. It does not delete Boxes or Agents the key created.

```bash theme={null}
box api-key usage sak_123
box api-key usage sak_123 --verbose
```

<Note>
  Creating, rotating, and revoking keys stay session-gated, except that an admin-scoped token can create a new key. A credential that could mint more credentials would defeat the point of least privilege. Use `box api-key create|rotate|revoke` after a browser sign-in, or the [API Keys](https://box.ascii.dev/box/dashboard?tab=api-keys) tab.
</Note>

## Errors

Scoped keys return typed 403s:

| `error`                    | Meaning                           |
| -------------------------- | --------------------------------- |
| `api_key_expired`          | TTL elapsed                       |
| `api_key_action_forbidden` | Action not on the key             |
| `api_key_box_forbidden`    | Box or environment not on the key |

The bearer header and SDK config are unchanged.

## Store keys

| Platform       | Store as                                             |
| -------------- | ---------------------------------------------------- |
| Railway        | Variable named `BOX_API_KEY`                         |
| GitHub Actions | Repository or environment secret named `BOX_API_KEY` |
| Docker Compose | Environment variable or secret named `BOX_API_KEY`   |
| Kubernetes     | Secret mounted or exposed as `BOX_API_KEY`           |

Do not put API keys in:

* Dockerfiles
* Images
* Source code
* Shell history
* Public CI logs

## Rotate a key

Rotating a key immediately revokes the old secret, preserves the API key id, and shows a new secret once. An expired scoped key cannot be rotated; the API returns `409 api_key_expired`, so create a replacement instead.

Use **Rotate** only when you can update the deployed secret immediately:

1. Rotate the key: `box api-key rotate <id>` (find ids with `box api-key list`), or use the dashboard.
2. Copy the new secret.
3. Update `BOX_API_KEY` in your platform secret manager.
4. Redeploy or restart workers that use the key.

To avoid downtime, create a second key first:

1. Create a new key.
2. Update the platform secret to the new key.
3. Redeploy or restart workers.
4. Revoke the old key after the new deployment is live.

## Revoke a key

Revoking a key immediately disables it. Existing CLI configs or running processes using that key will fail the next Box API request with an auth error. Boxes and Agents that key created stay. `box api-key usage <id>` still shows their totals.

Do not put keys in Dockerfiles, images, source, shell history, or public CI logs.

Production guidance: one key per job, shortest TTL you can live with, `read-only` or `ci` unless you truly need `admin`. Reissue legacy keys.

## Rotate and revoke

Rotate keeps the id, scope, and expiry date, replaces the secret, and shows the new secret once. It does not extend the key's lifetime or convert a legacy key into a scoped key. To avoid downtime, create a second key, cut over, then revoke the old one.

`box api-key revoke <id>` or **Revoke** in the dashboard. The next request with that secret fails auth.
