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

# Capability Tokens API: Mint Scoped Sandbox Credentials

> Mint short-lived capability tokens scoped to a sandbox epoch. Use tokens to authenticate data plane requests and to grant scoped browser or third-party access.

Capability tokens are the credential type for the sandbox data plane. They are short-lived, cryptographically signed, and bound to a specific sandbox epoch. Unlike your organisation API key, a capability token carries explicit scope information — the sandbox ID, the epoch, a port restriction, and an expiry — so the gateway and the owning node can verify and enforce it without contacting the control plane.

You mint capability tokens from the control plane using your organisation API key. The token you get back is what you (or a browser client) send to the data plane using the `X-Sandbox-Token` header.

## Why capability tokens exist

Every sandbox data plane endpoint requires a capability token rather than your API key for two reasons. First, your organisation API key is a long-lived secret that should never travel to a sandbox agent. Second, when you want to grant a browser or a third-party service access to exactly one port on one sandbox for a limited time, you mint an attenuated token scoped to that port — no broader access is possible, even if the token is intercepted.

## POST /v1/sandboxes/{sandboxId}/token

Mints a fresh capability token for the sandbox's current epoch. The request body is optional — omit it to get a full-scope token with the installation's default TTL.

```bash theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
curl -X POST https://api.example.com/v1/sandboxes/<sandbox-id>/token \
  -H "X-API-Key: <your-api-key>" \
  -H "Content-Type: application/json" \
  -d '{
    "ttl_seconds": 300,
    "ports": [3000, 8080]
  }'
```

**Path parameters**

<ParamField path="sandboxId" type="string" required>
  The 24-character sandbox NanoID.
</ParamField>

**Request body** (optional)

<ParamField body="ttl_seconds" type="integer">
  Requested token lifetime in seconds. Bounded by the installation's maximum
  configured TTL. Omit to use the installation default.
</ParamField>

<ParamField body="ports" type="integer[]">
  Restrict the token to these specific ports. A token with a port restriction
  is rejected by the gateway for any port not in the list. Omit this field for
  a full-scope token that permits all ports your credential allows. You cannot
  mint a token with broader scope than your own credential grants. Up to 16
  entries, each between 1 and 65535.
</ParamField>

**Response** — `201 Created`

```json theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
{
  "token": "eyJhbGciOiJFZERTQSJ9...",
  "expires_at": "2024-11-01T12:10:00Z",
  "epoch": 2,
  "ports": [3000, 8080]
}
```

<ResponseField name="token" type="string" required>
  The capability token string. Pass this in the `X-Sandbox-Token` header on
  data plane requests. Never log it.
</ResponseField>

<ResponseField name="expires_at" type="string" required>
  ISO 8601 timestamp after which the token is rejected. Mint a new token before
  this time if you need continued access.
</ResponseField>

<ResponseField name="epoch" type="integer" required>
  The sandbox epoch this token was minted against. If the sandbox is paused and
  resumed before the token expires, the token is still invalidated because the
  epoch advances on resume.
</ResponseField>

<ResponseField name="ports" type="integer[]">
  The port scope, when you requested a restriction. Null means full scope.
</ResponseField>

**Error codes**: `400` malformed request · `401` unauthenticated · `404` sandbox not found · `409` lifecycle conflict (sandbox not in a state that allows token minting)

## Token lifetime and invalidation

A capability token is valid until the earlier of two events:

1. The `expires_at` timestamp passes.
2. The sandbox epoch advances — which happens on every resume after a pause.

A paused sandbox cannot serve data plane requests. When you resume it, the new `SandboxWithToken` response includes a fresh token for the new epoch. All tokens minted against the previous epoch fail closed immediately.

<Note>
  The FissionPlane TypeScript SDK manages token lifecycle automatically. It stores
  the token returned from sandbox create and resume, re-mints automatically when
  a token is about to expire, and replaces the token on the handle after resume.
  You only need this endpoint if you are calling the data plane directly — for
  example, from a browser application or a language without an official SDK.
</Note>

## Attenuated tokens for browser access

To give a browser or untrusted client access to a specific port on your sandbox, mint a token scoped to just that port with a short TTL:

```bash theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
curl -X POST https://api.example.com/v1/sandboxes/<sandbox-id>/token \
  -H "X-API-Key: <your-api-key>" \
  -H "Content-Type: application/json" \
  -d '{
    "ttl_seconds": 3600,
    "ports": [3000]
  }'
```

The resulting token can only reach port `3000` on that sandbox and expires in one hour. Even if a user inspects it, they cannot elevate it to access other ports or other sandboxes.

<Warning>
  Never pass your organisation API key to the data plane or to browser code.
  Use capability tokens for all data plane access and for any credential you
  hand to a client outside your server.
</Warning>

## Using a token on the data plane

Once you have minted a token, include it in the `X-Sandbox-Token` header on every data plane request:

```bash theme={"theme":{"light":"github-light","dark":"github-dark-default"}}
curl -X POST https://50000-<sandbox-id>.sandboxes.example.com/commands \
  -H "X-Sandbox-Token: <capability-token>" \
  -H "Content-Type: application/json" \
  -d '{"command": "node", "args": ["--version"]}'
```

For WebSocket connections (process streams and file watches), pass the token as a subprotocol in the upgrade handshake. See [Streaming](/docs/api/streaming) for details.
