> ## 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 Boat API keys. Least privilege for production.

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

<Warning>
  Treat API keys as secrets. Store them as `BOAT_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 sandbox is in the key's sandbox set, or in one of its environments.

Default deny. Unknown routes are refused for scoped keys.

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

### Actions

| Action                                   | What it unlocks                                      |
| ---------------------------------------- | ---------------------------------------------------- |
| `sandbox.create`                         | Create a sandbox                                     |
| `sandbox.read`                           | List and inspect sandboxes                           |
| `sandbox.update`                         | Rename, recover, and other sandbox writes            |
| `sandbox.stop`                           | Stop                                                 |
| `sandbox.resume`                         | Resume                                               |
| `sandbox.fork`                           | Fork                                                 |
| `sandbox.delete`                         | Delete                                               |
| `agent.prompt`                           | Prompt the sandbox 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 sandboxes, files, snapshots, environments                          |
| `full-sandbox` | Operate a sandbox without create / resume / fork / delete / account writes |
| `ci`           | Create, run, snapshot. No account admin                                    |
| `admin`        | `*`                                                                        |

### In-sandbox key

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

## Create a key

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

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

`--ttl` max is 365 days. `--sandbox` 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}
BOAT_API_KEY="$(boat 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 sandbox / environment selectors.

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

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

  ```ts TypeScript theme={null}
  import { BoatApi, Configuration } from "@boatdev/sdk";

  const sandbox = new BoatApi(new Configuration({
    basePath: "https://boat.dev/api/v1",
    accessToken: process.env.BOAT_API_KEY!,
  }));

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

  ```python Python theme={null}
  import os
  from boat_sdk import ApiClient, Configuration
  from boat_sdk.api.boat_api import BoatApi

  config = Configuration(host="https://boat.dev/api/v1", access_token=os.environ["BOAT_API_KEY"])
  with ApiClient(config) as client:
      sandbox = BoatApi(client)
      me = sandbox.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 sandboxes 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}
boat api-key list
boat api-key list --all    # include per-sandbox machine keys
```

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

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

## See usage for one key

`boat api-key usage <id>` prints the same 30-day request total and live resource count. Add `--verbose` for the sandboxes/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 sandboxes or Agents the key created.

```bash theme={null}
boat api-key usage sak_123
boat 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 `boat api-key create|rotate|revoke` after a browser sign-in, or the [API Keys](https://boat.dev/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_sandbox_forbidden` | Boat or environment not on the key |

The bearer header and SDK config are unchanged.

## Store keys

| Platform       | Store as                                              |
| -------------- | ----------------------------------------------------- |
| Railway        | Variable named `BOAT_API_KEY`                         |
| GitHub Actions | Repository or environment secret named `BOAT_API_KEY` |
| Docker Compose | Environment variable or secret named `BOAT_API_KEY`   |
| Kubernetes     | Secret mounted or exposed as `BOAT_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: `boat api-key rotate <id>` (find ids with `boat api-key list`), or use the dashboard.
2. Copy the new secret.
3. Update `BOAT_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 Boat API request with an auth error. Sandboxes and Agents that key created stay. `boat 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.

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