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
}' {
"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
}shellstring- Only /bin/bash for now, which is also the default.
columnsinteger- 20 to 500. Defaults to 120.
rowsinteger- 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.
// `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
| Frame | Payload | Meaning |
|---|---|---|
| binary | raw bytes | Keystrokes and input for the shell. |
| text | {"type":"resize","columns":120,"rows":32} | Resize the terminal. |
| text | {"type":"close"} | Close the terminal. |
You receive
| Frame | Payload | Meaning |
|---|---|---|
| text | {"type":"ready"} | The shell is attached; start sending. |
| binary | raw bytes | Output 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 shellandspawn attachdo all of this for you; see the spawn CLI.