Skip to main content
A running workspace exposes one realtime connection: a multiplexed WebSocket that carries terminal I/O, file operations, live agent events, port changes, and git diffs over a single socket. The web app and the aether CLI both speak this same protocol.

Endpoint

{id} is the workspace ID. This endpoint sits on the non-versioned router — it is not under /v1 like the rest of the public API. The API authenticates the request, verifies you own the workspace, requires the workspace to be in the Ready display state, then reverse-proxies raw text frames to the workspace VM. Every other documented surface is REST under /v1; this is the only WebSocket.

Connecting

Open the socket through the connect handshake rather than guessing the URL. Call POST /v1/workspaces/{id}/connect first — it returns a state-discriminated result. Connect never boots the VM unless you ask it to. Omitting start attaches only: a workspace that is not already usable answers 409 instead of being started, so merely viewing or polling a workspace can never bring one up (or undo a suspend). Booting is an explicit act — ?start=true&task_id=..., where task_id attributes the runtime to a task.
1

Request a connection

Call POST /v1/workspaces/{id}/connect. Add start=true&task_id=... only if you intend to boot a stopped workspace — send it once, on the request that expresses that intent.
2

Poll while connecting

While the VM boots, the response is state: "connecting" with retry_after_ms (1000). Wait that long and call again without start. The first accepted response has already created whatever start it was going to; repeating start=true can authorize a second one.A transitional 409 is different: it was refused, so nothing was started and an unspent start=true may be re-sent on the retry.
3

Open the socket

Once the response is state: "running", it includes transport.websocket_path (/workspaces/{id}/ws) and a preview_token. Connect to websocket_path, passing your token (see below).

When connect refuses

A 409 means the workspace is not attachable. The body is discriminated by kind, and the kind IS the answer — there is no flag to interpret: A workspace that is down can answer either arm. startable means this endpoint can bring it back — including resuming a suspended or stopped workspace whose run is still active; not_connectable means it cannot, because there is no machine left to start or the workspace failed. Either way that workspace is usually still recoverable by sending the agent a message, which goes through the task path rather than this endpoint.
The running response shape:
The preview_token is a 32-character lowercase alphanumeric string used to build preview URLs.

Authentication

Pass an Aether API key (aether_…) or a session JWT one of three ways: Browsers can’t set request headers on a WebSocket, so the web client uses the query parameter. When a subprotocol is sent, the server echoes Sec-WebSocket-Protocol: bearer.

Message Format

Every message is JSON with a channel field identifying the target subsystem and a type field identifying the message within that channel. Six channels are multiplexed over the connection: Client messages are validated against strict schemas — extra or misnamed fields are rejected. The canonical shapes live in packages/workspace-protocol/src/messages.ts.

The error Channel

The server pushes errors on a dedicated channel that you never send to. It is emitted after the socket opens when the VM fails to initialize or an inbound message fails to parse.
If the API proxy cannot establish its connection to the VM, the browser WebSocket is not upgraded. The handshake returns HTTP 502 instead. The web client treats this as a failed connection attempt and re-runs the connect sequence so /connect can re-resolve the current workspace transport; other clients receive the failed handshake and can decide whether to retry.

Connection Keepalive

The API sends WebSocket ping frames every 15 seconds. If no pong arrives within 120 seconds, it closes the connection.

Reconnection

On reconnect, what comes back depends on the channel:
  • Terminal sessions keep running on the VM while you’re disconnected; reattach by sending create with the same sessionId.
  • Ports sends a fresh snapshot of currently-open ports on connect.
  • Agent resumes only live task_event messages for tasks you re-subscribe to. Conversation history is not replayed over the socket — fetch it via REST (conversation messages).
  • Files does not replay change events that occurred while disconnected; re-list or read to get current state.
Prompting an agent, responding to its questions, and stopping a task all happen over REST, not the WebSocket. See the agent channel.