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. CallPOST /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
A409 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.
running response shape:
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 achannel 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.
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
createwith the samesessionId. - Ports sends a fresh
snapshotof currently-open ports on connect. - Agent resumes only live
task_eventmessages for tasks you re-subscribeto. 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-
listorreadto 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.