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

# Terminal WebSocket

> Interactive PTY shell over WebSocket with real-time bidirectional terminal I/O.

Connect to an interactive terminal session on a computer over WebSocket. You get a full PTY, so anything that works in a real shell works here: colours, curses apps, job control.

## Connection URL

```text theme={null}
wss://www.orgo.ai/desktops/{instance_id}/ws/terminal?token={password}
```

`{instance_id}` is the computer's `instance_id`, returned by [Create computer](/api-reference/computers/create). It is also the last path segment of the computer's `connection_url`.

### Authentication

Pass the computer's password as the `token` query parameter. It is the same password used for VNC. Retrieve it from [Get VNC password](/api-reference/computers/vnc-password) before connecting. That endpoint takes the computer's UUID, not its `instance_id`. You need both identifiers: the UUID to fetch the password, the `instance_id` to build this URL.

Send the password in `?token=`, from a server or a browser. An `Authorization` header does not work on this socket, whether it carries an API key or the password: orgo-web accepts it, but it is not forwarded to the computer, which authenticates only `?token=`. The connection opens and then closes with `1000`. An API key in `?token=` fails the same way, and from a browser (any connection with an `Origin` header) it is refused with `4001`. Never put an account key in browser code.

A connection with no usable credential is closed with code `4001`. See [Close codes](#close-codes).

### Query parameters

<ParamField query="token" type="string" required>
  Computer password, from [Get VNC password](/api-reference/computers/vnc-password). Optional only when the request carries a signed-in orgo.ai session cookie, in which case orgo-web supplies the password for you.
</ParamField>

<ParamField query="cols" type="number" default="80">
  Terminal width in columns. A missing or unparseable value falls back to 80.
</ParamField>

<ParamField query="rows" type="number" default="24">
  Terminal height in rows. A missing or unparseable value falls back to 24.
</ParamField>

<ParamField query="session" type="string">
  Name of a persistent shell session. Must be 1-32 characters of `A-Z`, `a-z`, `0-9`, `-` or `_`.

  With a valid `session`, the shell survives disconnection: reconnecting with the same name re-attaches to the running session, with any long job still going. Omit it, or send a name that fails validation, and you get an ephemeral shell (`bash -l` on Linux, PowerShell on Windows) that is killed when the socket closes.
</ParamField>

## Message protocol

Messages are JSON text frames in both directions.

### Client → server messages

<AccordionGroup>
  <Accordion title="input" icon="keyboard">
    Send keyboard input to the terminal.

    ```json theme={null}
    {
      "type": "input",
      "data": "ls -la\r"
    }
    ```

    <ParamField body="type" type="string" required>
      Must be `"input"`.
    </ParamField>

    <ParamField body="data" type="string" required>
      The input string to send. Use `\r` for the Enter key.
    </ParamField>
  </Accordion>

  <Accordion title="resize" icon="arrows-maximize">
    Resize the terminal dimensions.

    ```json theme={null}
    {
      "type": "resize",
      "cols": 120,
      "rows": 40
    }
    ```

    <ParamField body="type" type="string" required>
      Must be `"resize"`.
    </ParamField>

    <ParamField body="cols" type="number" required>
      New number of columns. The resize is ignored unless both `cols` and `rows` are above `0`.
    </ParamField>

    <ParamField body="rows" type="number" required>
      New number of rows. The resize is ignored unless both `cols` and `rows` are above `0`.
    </ParamField>
  </Accordion>

  <Accordion title="ping" icon="heart-pulse">
    Ask for a `pong`.

    ```json theme={null}
    {
      "type": "ping"
    }
    ```
  </Accordion>
</AccordionGroup>

<Note>
  A text frame that is not valid JSON is written to the PTY verbatim, as raw keystrokes. This keeps older raw-mode clients working. A frame that parses as JSON but carries an unrecognised `type` is discarded silently.
</Note>

### Server → client messages

<AccordionGroup>
  <Accordion title="output" icon="terminal">
    Terminal output data.

    ```json theme={null}
    {
      "type": "output",
      "data": "root@computer:~# "
    }
    ```

    <ResponseField name="type" type="string">
      Always `"output"`.
    </ResponseField>

    <ResponseField name="data" type="string">
      The terminal output. May contain ANSI escape codes for colours and formatting.
    </ResponseField>
  </Accordion>

  <Accordion title="error" icon="circle-exclamation">
    The shell could not be started.

    ```json theme={null}
    {
      "type": "error",
      "message": "failed to start terminal: fork/exec /bin/bash: no such file or directory"
    }
    ```

    <ResponseField name="type" type="string">
      Always `"error"`.
    </ResponseField>

    <ResponseField name="message" type="string">
      Human-readable error description.
    </ResponseField>

    This frame is followed immediately by a normal (`1000`) close, not by an error close code. Watch for it in your message handler. A client that only inspects close codes sees a clean disconnect and never learns why.
  </Accordion>

  <Accordion title="exit" icon="right-from-bracket">
    The shell process has exited.

    ```json theme={null}
    {
      "type": "exit",
      "code": 0
    }
    ```

    <ResponseField name="type" type="string">
      Always `"exit"`.
    </ResponseField>

    <ResponseField name="code" type="number">
      Exit code of the shell process, or `-1` when it could not be determined.
    </ResponseField>
  </Accordion>

  <Accordion title="pong" icon="heart-pulse">
    Response to a `ping`.

    ```json theme={null}
    {
      "type": "pong"
    }
    ```
  </Accordion>
</AccordionGroup>

## Examples

<CodeGroup>
  ```javascript JavaScript theme={null}
  // COMPUTER_ID is the computer's UUID. INSTANCE_ID is its instance_id, which
  // fills the /desktops/{instance_id} slot. Create computer returns both.
  const computerId = process.env.COMPUTER_ID;
  const instanceId = process.env.INSTANCE_ID;
  const apiKey = process.env.ORGO_API_KEY;

  // Step 1: Get the computer password. This endpoint takes the UUID.
  const res = await fetch(
    `https://www.orgo.ai/api/computers/${computerId}/vnc-password`,
    { headers: { 'Authorization': `Bearer ${apiKey}` } }
  );
  const { password } = await res.json();

  // Step 2: Connect with the password as token
  const ws = new WebSocket(
    `wss://www.orgo.ai/desktops/${instanceId}/ws/terminal?token=${password}&cols=80&rows=24`
  );

  ws.onopen = () => {
    console.log('Connected to terminal');
    // Example: run a command once the socket is open
    sendCommand('echo "Hello, World!"');
  };

  ws.onmessage = (event) => {
    const message = JSON.parse(event.data);

    switch (message.type) {
      case 'output':
        // Print the terminal output
        process.stdout.write(message.data);
        break;
      case 'error':
        console.error('Terminal error:', message.message);
        break;
      case 'exit':
        console.log('Shell exited with code:', message.code);
        break;
    }
  };

  ws.onclose = (e) => {
    if (e.code === 4008) console.log('Disconnected for inactivity. Reconnect to resume');
  };

  // Send a command
  function sendCommand(command) {
    ws.send(JSON.stringify({
      type: 'input',
      data: command + '\r'  // \r for Enter key
    }));
  }

  // Resize terminal
  function resizeTerminal(cols, rows) {
    ws.send(JSON.stringify({
      type: 'resize',
      cols,
      rows
    }));
  }
  ```

  ```python Python theme={null}
  import asyncio
  import websockets
  import json
  import os
  import requests

  async def connect_terminal(computer_id: str, instance_id: str, api_key: str, cols: int = 80, rows: int = 24):
      # Step 1: Get the computer password. This endpoint takes the UUID.
      res = requests.get(
          f"https://www.orgo.ai/api/computers/{computer_id}/vnc-password",
          headers={"Authorization": f"Bearer {api_key}"}
      )
      password = res.json()["password"]

      # Step 2: Connect with the password as token. Adding &session=build gives a
      # shell that survives a reconnect.
      url = f"wss://www.orgo.ai/desktops/{instance_id}/ws/terminal?token={password}&cols={cols}&rows={rows}"

      async with websockets.connect(url) as ws:
          # Handle incoming messages
          async def receive_messages():
              async for message in ws:
                  data = json.loads(message)

                  if data["type"] == "output":
                      print(data["data"], end="", flush=True)
                  elif data["type"] == "error":
                      print(f"Error: {data['message']}")
                  elif data["type"] == "exit":
                      print(f"Shell exited with code: {data['code']}")
                      break

          # Send a command
          async def send_command(command: str):
              await ws.send(json.dumps({
                  "type": "input",
                  "data": command + "\r"
              }))

          # Start receiving messages in background
          receive_task = asyncio.create_task(receive_messages())

          # Send commands
          await send_command("echo 'Hello, World!'")
          await send_command("ls -la")

          # Wait for output
          await asyncio.sleep(2)
          receive_task.cancel()

  # COMPUTER_ID is the UUID. INSTANCE_ID fills the /desktops/{instance_id} slot.
  asyncio.run(connect_terminal(
      os.environ["COMPUTER_ID"], os.environ["INSTANCE_ID"], os.environ["ORGO_API_KEY"]
  ))
  ```

  ```typescript TypeScript theme={null}
  interface TerminalMessage {
    type: 'output' | 'error' | 'exit' | 'pong';
    data?: string;
    message?: string;
    code?: number;
  }

  class TerminalConnection {
    private ws!: WebSocket;

    static async connect(computerId: string, instanceId: string, apiKey: string, cols = 80, rows = 24) {
      const conn = new TerminalConnection();

      // Step 1: Get the computer password. This endpoint takes the UUID.
      const res = await fetch(
        `https://www.orgo.ai/api/computers/${computerId}/vnc-password`,
        { headers: { 'Authorization': `Bearer ${apiKey}` } }
      );
      const { password } = await res.json();

      // Step 2: Connect with the password as token
      const url = `wss://www.orgo.ai/desktops/${instanceId}/ws/terminal?token=${password}&cols=${cols}&rows=${rows}`;
      conn.ws = new WebSocket(url);

      conn.ws.onmessage = (event) => {
        const message: TerminalMessage = JSON.parse(event.data);
        conn.handleMessage(message);
      };

      // Wait for the socket to open, so send() can be called right away
      await new Promise<void>((resolve, reject) => {
        conn.ws.onopen = () => resolve();
        conn.ws.onerror = () => reject(new Error('Terminal connection failed'));
      });

      return conn;
    }

    private handleMessage(message: TerminalMessage) {
      switch (message.type) {
        case 'output':
          console.log(message.data);
          break;
        case 'error':
          console.error('Error:', message.message);
          break;
        case 'exit':
          console.log('Exited with code:', message.code);
          break;
      }
    }

    send(data: string) {
      this.ws.send(JSON.stringify({ type: 'input', data }));
    }

    resize(cols: number, rows: number) {
      this.ws.send(JSON.stringify({ type: 'resize', cols, rows }));
    }

    ping() {
      this.ws.send(JSON.stringify({ type: 'ping' }));
    }

    close() {
      this.ws.close();
    }
  }

  // COMPUTER_ID is the UUID. INSTANCE_ID fills the /desktops/{instance_id} slot.
  const terminal = await TerminalConnection.connect(
    process.env.COMPUTER_ID!,
    process.env.INSTANCE_ID!,
    process.env.ORGO_API_KEY!
  );
  terminal.send('echo "Hello!"\r');
  ```
</CodeGroup>

## Integration with xterm.js

For browser-based terminal UIs, use [xterm.js](https://xtermjs.org/):

```javascript theme={null}
import { Terminal } from '@xterm/xterm';
import { FitAddon } from '@xterm/addon-fit';
import '@xterm/xterm/css/xterm.css';

// Initialize xterm.js
const terminal = new Terminal({
  cursorBlink: true,
  fontFamily: 'monospace',
  fontSize: 14,
});

const fitAddon = new FitAddon();
terminal.loadAddon(fitAddon);
terminal.open(document.getElementById('terminal'));
fitAddon.fit();

// Step 1: Get the computer password.
//
// In a browser, have YOUR server call GET /api/computers/{id}/vnc-password with
// the Orgo API key and hand the page only the password. That endpoint takes the
// computer's UUID; the socket URL below takes its instance_id. An `sk_` key in
// ?token= is refused whenever the request carries an Origin header, and it is a
// full account credential besides.
const computerId = 'a3bb189e-8bf9-3888-9912-ace4e6543002';
const instanceId = 'a3881618';
const { password } = await fetchPasswordFromYourBackend(computerId);

// Step 2: Connect with the password as token
const ws = new WebSocket(
  `wss://www.orgo.ai/desktops/${instanceId}/ws/terminal?token=${password}&cols=${terminal.cols}&rows=${terminal.rows}`
);

// Handle output from server
ws.onmessage = (event) => {
  const message = JSON.parse(event.data);
  if (message.type === 'output') {
    terminal.write(message.data);
  }
};

// Send user input to server
terminal.onData((data) => {
  ws.send(JSON.stringify({ type: 'input', data }));
});

// Handle terminal resize
window.addEventListener('resize', () => {
  fitAddon.fit();
  ws.send(JSON.stringify({
    type: 'resize',
    cols: terminal.cols,
    rows: terminal.rows
  }));
});
```

## Best practices

<CardGroup cols={2}>
  <Card title="Heartbeat" icon="heart-pulse">
    Send a `ping` every 30 seconds to detect a dead connection early. Each `ping` is a message you send, so it also counts as activity for the idle timeout.
  </Card>

  <Card title="Reconnection" icon="rotate">
    Reconnect with exponential backoff, starting at 2 seconds. Pair it with a `session` name so the shell you reconnect to is the one you left.
  </Card>

  <Card title="Resize events" icon="maximize">
    Send a `resize` whenever the terminal container changes size, so text wraps correctly.
  </Card>

  <Card title="ANSI support" icon="palette">
    Output contains ANSI escape codes. Use a library like xterm.js that handles them.
  </Card>
</CardGroup>

<Note>
  The terminal WebSocket gives direct root shell access. To run one command programmatically, use [Execute bash](/api-reference/computers/bash) instead.
</Note>

## Idle disconnect

An idle terminal is closed with code `4008`. Traffic in either direction counts as activity, so a long build streaming output keeps the socket open even with nobody typing. Activity is shared per computer and caller, so input you send over a VNC connection to the same computer keeps this one alive too. In production the window is 30 minutes. Reconnect to resume; with a `session` name your shell is still there.

## Close codes

| Code | Meaning |
| - | - |
| `1000` | Normal closure. Also what you get after an `error` or `exit` frame, when the shell process ends, and when the computer rejects the credential (see [Authentication](#authentication)). |
| `1001` | orgo-web is draining for a restart or deploy. Reconnect. |
| `1006` | Dropped with no close frame. The server terminates a socket that stops answering its 30-second protocol ping. |
| `4001` | Authentication required. No session cookie and no valid `token`, a cookie-authenticated browser connecting from a disallowed origin, or a caller whose workspace role is view-only. |
| `4003` | The computer exists but is not running or not ready, or its workspace is not active. Also sent to an open connection when your workspace access changes or a trial ends. Start the computer, then reconnect. |
| `4004` | No computer with that id. |
| `4006` | The API key is valid but scoped to a different workspace. |
| `4007` | MFA is required for this session. Re-challenge, then reconnect. |
| `4008` | Idle timeout. See [Idle disconnect](#idle-disconnect). |
| `4500` | Internal proxy error. |
| `4502` | orgo-web has no route to the computer's host. Retry; if it persists, contact support. |
| `4503` | orgo-web could not look up the computer. Retry. |

A URL that is not a `/desktops/{instance_id}/ws/...` path is refused before the WebSocket opens, so it gets no close code.


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