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

# Connect to a Sandbox

> Open an interactive terminal or a preview URL for a running sandbox, and see why SSH is not offered.

A terminal gives you a shell in a running sandbox, and a preview gives you a URL for a port a server listens on. Name `terminal` or `previews` in `requirements` when you create the sandbox. BoxRouter then uses only a route that provides the capability, and `contract.gates` records what you got.

**Before You Begin:** `BOXROUTER_API_KEY` set and a client installed, as in the [quickstart](https://boxrouter.ai/docs/quickstart.md).

## Open a Terminal

A terminal is a real PTY running a Bash login shell. The image needs `python3`, which the shell's helper uses. The shell ends when it exits, when you kill it, when `timeout_seconds` passes (1 to 3,600; default 600) or when the sandbox ends.

`terminal.output()` yields raw bytes until the shell exits. The SDK examples send a command that exits so the stream finishes. The sandbox scope then closes its terminal handles and destroys the sandbox.

```python Python theme={"theme":"css-variables"}
import sys
from boxrouter import BoxRouter

with BoxRouter() as client:
    with client.sandboxes.create(requirements=["terminal"]) as box:
        terminal = box.open_terminal()
        terminal.write(b"printf 'hello\\n'; exit 3\n")
        for chunk in terminal.output():
            sys.stdout.buffer.write(chunk)
        sys.stdout.buffer.flush()
        print("exit code:", terminal.exit_code)
```

```typescript TypeScript theme={"theme":"css-variables"}
import { BoxRouter } from '@boxrouter/sdk';

await using client = new BoxRouter();
await using box = await client.sandboxes.create({ requirements: ['terminal'] });
const terminal = await box.openTerminal();
await terminal.write(new TextEncoder().encode("printf 'hello\\n'; exit 3\n"));
for await (const chunk of terminal.output()) process.stdout.write(chunk);
console.log('exit code:', terminal.exitCode);
```

```bash CLI theme={"theme":"css-variables"}
(
  set -euo pipefail
  ID=$(boxrouter create --require terminal)
  trap 'boxrouter destroy "$ID"' EXIT
  boxrouter terminal "$ID"
)
```

The excerpt below omits the prompt, the echoed command and the shell's `logout` line. The shell confirms its exit with status 3.

Output

```text theme={"theme":"css-variables"}
hello
exit code: 3
```

`output()` returns exact TTY bytes, including shell prompts, echoed input, and ANSI escape sequences.
Use [commands](https://boxrouter.ai/docs/commands.md) when you need program stdout without terminal formatting.

Decode the bytes as text only when needed. Input also goes in as bytes, up to 64 KiB per write, with no newline added; await each write before sending the next. A confirmed exit event sets `terminal.exit_code` / `terminal.exitCode`; until then the value is `None` / `null`, which means unknown.

`Ctrl-]` ends the CLI stream and leaves the remote shell running. The CLI example's trap then destroys its sandbox. Remove that trap to keep the sandbox and shell running.

## Reconnect

If an output stream ends unexpectedly, call `terminal.output()` again on the same handle to resume after its last consumed event. Reconnecting never repeats writes. A replay gap raises an error; the SDK does not silently skip missing output.

Closing a terminal handle ends its local stream; the remote shell and sandbox keep running.

## Advanced: Resume from a Saved Cursor

Persist `terminal.id` and `terminal.last_event_id` before disconnecting. In a new process, open the same terminal
and pass the saved cursor to `output()`. This resumes output after the last event you consumed; never resend input.

```python Python theme={"theme":"css-variables"}
import sys
from boxrouter import BoxRouter

sandbox_id = "<SANDBOX_ID>"
terminal_id = "<TERMINAL_ID>"
last_event_id = "<LAST_EVENT_ID>"

with BoxRouter() as client:
    terminal = client.sandboxes.open_terminal(sandbox_id, terminal_id=terminal_id)
    try:
        for chunk in terminal.output(last_event_id=last_event_id):
            sys.stdout.buffer.write(chunk)
    finally:
        terminal.close()
```

```typescript TypeScript theme={"theme":"css-variables"}
import { BoxRouter } from '@boxrouter/sdk';

const sandboxId = '<SANDBOX_ID>';
const terminalId = '<TERMINAL_ID>';
const lastEventId = '<LAST_EVENT_ID>';
await using client = new BoxRouter();
const terminal = await client.sandboxes.openTerminal(sandboxId, { terminalId });
try {
  for await (const chunk of terminal.output({ lastEventId })) process.stdout.write(chunk);
} finally {
  terminal.close();
}
```

```bash CLI theme={"theme":"css-variables"}
boxrouter terminal "$SANDBOX_ID" --terminal-id "$TERMINAL_ID" --last-event-id "$LAST_EVENT_ID"
```

The CLI does not retry automatically; after an abrupt stream end, choose when to run the printed resume command.

### Raw Events and Replay

`terminal.events()` exposes typed output, exit and error events for clients that need event metadata. Raw REST output
carries Base64 bytes. BoxRouter retains a 1 MiB sliding window for 60 seconds after the shell exits; a cursor outside
that window returns `410`.

See the [terminal event reference](https://boxrouter.ai/docs/reference/stream-terminal-events.md), [terminal input reference](https://boxrouter.ai/docs/reference/write-terminal-input.md)
and [terminal open reference](https://boxrouter.ai/docs/reference/open-terminal.md) for the raw operations.

## Exit Status and Restarts

Until BoxRouter confirms the exit, `exit_code` is null. Treat null as unknown, not success. A killed terminal reports `-1`; a hang-up reports 129.

## Open a Preview

A preview is an HTTPS URL for one port in the sandbox. Name the ports and the kind of link you accept when you create the sandbox; opening any other port fails with `not_named`. If no route supports public previews, creation fails with `no_eligible_route`.

Start a server and wait for its port before opening the preview. The SDK and CLI helpers return when the port accepts connections inside the sandbox.

Each example requests sandbox deletion on exit; check the sandbox list if cleanup fails before retrying.

```python Python theme={"theme":"css-variables"}
from boxrouter import BoxRouter

with BoxRouter() as client:
    with client.sandboxes.create(
        requirements={"previews": {"access": "public", "ports": [3000]}},
    ) as box:
        box.processes.start("python3 -m http.server 3000", wait_for_port=3000)
        print(box.previews.open(3000).url)
        input("Press Enter to destroy the sandbox: ")
```

```typescript TypeScript theme={"theme":"css-variables"}
import { createInterface } from 'node:readline/promises';
import { BoxRouter } from '@boxrouter/sdk';

await using client = new BoxRouter();
await using box = await client.sandboxes.create({ requirements: { previews: { access: 'public', ports: [3000] } } });
await box.processes.start('python3 -m http.server 3000', { wait_for_port: 3000 });
console.log((await box.previews.open(3000)).url);
const prompt = createInterface({ input: process.stdin, output: process.stdout });
try {
  await prompt.question('Press Enter to destroy the sandbox: ');
} finally {
  prompt.close();
}
```

```bash CLI theme={"theme":"css-variables"}
(
  set -euo pipefail
  ID=$(boxrouter create --requirements '{"previews":{"access":"public","ports":[3000]}}')
  trap 'boxrouter destroy "$ID"' EXIT
  boxrouter process start "$ID" 'python3 -m http.server 3000' --wait-for-port 3000 >/dev/null
  boxrouter preview "$ID" 3000
  read -r -p "Press Enter to destroy the sandbox: "
)
```

```bash HTTP theme={"theme":"css-variables"}
(
  set -euo pipefail
  API="${BOXROUTER_BASE_URL:-https://api.boxrouter.ai/api/v1}"
  AUTH="Authorization: Bearer $BOXROUTER_API_KEY"
  id=$(curl -sSf -X POST "$API/sandboxes" -H "$AUTH" -H 'Content-Type: application/json' \
    -d '{"requirements":{"previews":{"access":"public","ports":[3000]}}}' | jq -er .id)
  trap 'curl -sSf -X DELETE "$API/sandboxes/$id" -H "$AUTH" >/dev/null' EXIT
  curl -sSf -X POST "$API/sandboxes/$id/processes" -H "$AUTH" -H 'Content-Type: application/json' \
    -d '{"command": "python3 -m http.server 3000", "wait_for_port": 3000}' >/dev/null
  curl -sSf -X POST "$API/sandboxes/$id/previews" -H "$AUTH" \
    -H 'Content-Type: application/json' -d '{"port":3000}' | jq -er '.url | strings | select(startswith("https://"))'
  read -r -p 'Press Enter to destroy the sandbox: '
)
```

If the process exits first, or the port stays closed for 30 seconds, the call ends the process and fails with `409 port_not_listening`, with the output so far in `details`.

Open the returned URL while the script waits; it lists the server's working directory. Press Enter to destroy the sandbox
and close the preview. A public preview is reachable by anyone with its URL. The CLI prints only the URL; HTTP and
the SDKs return an object like this:

Output

```json theme={"theme":"css-variables"}
{
"id": "pv_97d12d193083",
"port": 3000,
"url": "https://<PREVIEW_HOST>",
"access": "public",
"credential": null,
"headers": {},
"expires_at": null,
"created_at": "2026-10-29T10:35:10.726567+00:00"
}
```

Choose the preview access you need:

* `access: "any"` accepts what the route offers. The result reports which access it provides.
* `public` or `private` requires that access. BoxRouter never falls back from private to public.

A private link carries its credential in either the URL (`credential: "link"`, for a clickable
link) or `headers`, which you send with every request.

The low-level API returns a URL even when nothing listens on the port. The SDK and CLI wait helpers above check that the server accepts connections first.

`expires_at` is set only when the provider enforces an expiry. You cannot shorten the link's lifetime.
Opening a port that already has a preview with the same access returns that preview.

Pausing and resuming a sandbox forgets its previews, because no route can read one back. Open the port again instead of caching the URL.

A preview stops answering when its sandbox is destroyed.

## Reach a Sandbox over SSH

The hosted API does not offer SSH access. Creation refuses `ssh` as a requirement. Use a terminal for an interactive shell and [commands](https://boxrouter.ai/docs/commands.md) for scripted work. [Provider support](https://boxrouter.ai/docs/provider-support.md) lists what the hosted API does not provide.

## Next Steps

> [card] [Run a Background Process](https://boxrouter.ai/docs/processes.md)
>
> Start a server that a preview can reach.

> [card] [Run Commands](https://boxrouter.ai/docs/commands.md)
>
> Run scripted work without a shell.

> [card] [Capabilities and Isolation](https://boxrouter.ai/docs/provider-support.md)
>
> Check which routes offer terminals and previews.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.