Sign in

Open it up

Terminals

Attach a live, interactive shell to a running sandbox over WebSocket — for you to watch an agent work, or for a human to take over.

Opening a terminal takes two steps. Your backend creates it with your API key and gets back a single-use token; then whoever needs the terminal — your backend or a browser — connects to the WebSocket with that token. Your API key never has to leave your server.

1. Create a terminal

POST /v1/sandboxes/{sandbox_id}/terminals

curl -sS -X POST "$SPAWN_API_URL/v1/sandboxes/$SANDBOX_ID/terminals" \
  -H "Authorization: Bearer $SPAWN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "shell": "/bin/bash",
    "columns": 120,
    "rows": 32
  }'
Response 201 Created
{
  "status": "success",
  "terminal": {
    "terminal_id": "term_PF9lwt4Vsn2C7DCBtqu3kNQn",
    "sandbox_id": "3ecb0221-faf1-4976-a8ae-6fd6262de59a",
    "state": "ready",
    "created_at": "2026-08-17T11:10:00Z",
    "token_expires_at": "2026-08-17T11:11:00Z"
  },
  "websocket_path": "/v1/terminals/term_PF9lwt4Vsn2C7DCBtqu3kNQn",
  "attachment_token": "<single-use-token>",
  "expires_in": 59
}
shell string
Only /bin/bash for now, which is also the default.
columns integer
20 to 500. Defaults to 120.
rows integer
5 to 200. Defaults to 32.

2. Connect to the WebSocket

WS /v1/terminals/{terminal_id}?token={attachment_token}

Open a WebSocket to websocket_path on the API host, with the token as a query parameter. The token works once and expires after 60 seconds; it replaces the API key for this connection.

JavaScript (browser, or Node 22+)
// `created` is the JSON body from step 1; `term` is an xterm.js Terminal
const url = new URL(created.websocket_path, 'wss://dev-api.renderops.com');
url.searchParams.set('token', created.attachment_token);

const ws = new WebSocket(url);
ws.binaryType = 'arraybuffer';

ws.onmessage = (e) => {
  if (typeof e.data !== 'string') {
    // Terminal output: raw bytes from the shell
    term.write(new Uint8Array(e.data));
    return;
  }
  const msg = JSON.parse(e.data);
  if (msg.type === 'ready') ws.send(new TextEncoder().encode('ls -la\n'));
  if (msg.type === 'exit') console.log('shell exited with', msg.exit_code);
  if (msg.type === 'error') console.error(msg.message);
};

In a browser, xterm.js renders the output and turns keystrokes into the bytes to send.

Messages

You send

FramePayloadMeaning
binaryraw bytesKeystrokes and input for the shell.
text{"type":"resize","columns":120,"rows":32}Resize the terminal.
text{"type":"close"}Close the terminal.

You receive

FramePayloadMeaning
text{"type":"ready"}The shell is attached; start sending.
binaryraw bytesOutput from the shell (stdout and stderr).
text{"type":"exit","exit_code":0}The shell exited.
text{"type":"error","message":"..."}The terminal failed.

Good to know

  • Closing the WebSocket closes the shell in the sandbox, and the other way round.
  • A sandbox with a terminal attached is never paused for being idle.
  • Pausing a sandbox closes its terminals; create a new one after resuming.
  • From your own terminal, spawn shell and spawn attach do all of this for you; see the spawn CLI.
Ctrl I