Skip to content
Raster
Esc
navigateopen⌘Jpreview
On this page

The realtime gateway

Live screen, input, terminal and event channels over one websocket protocol.

Frames, input and pty bytes do not go through the API — they go through a websocket gateway the API mints a ticket for. The dashboard’s desktop, terminal and file views run on it, and so do machine.screen.stream() and machine.terminal.connect().

Getting a ticket

POST /v1/machines/{machine_id}/connect
  ->  { token, gateway_url, expires_at, capabilities }

The ticket is short-lived and carries no credential of yours. The gateway exchanges it server-side for the machine’s actual connection details, so the machine’s address and its guest bearer token never cross into a browser.

capabilities is the subset of screen:read, input:write and terminal:write this ticket was granted. A ticket is scoped to one machine and one session.

Connecting

wss://gateway.example/machines/{machine_id}/screen
wss://gateway.example/machines/{machine_id}/input
wss://gateway.example/machines/{machine_id}/terminal/{terminal_id}
wss://gateway.example/machines/{machine_id}/events

The token is presented as the websocket subprotocol:

Sec-WebSocket-Protocol: computer.v1, ticket.<token>

Not a query parameter: query strings land in access logs, proxy logs and browser history, and this token is a credential. Browsers cannot set headers on a websocket handshake, so the subprotocol field is the only place left.

Close codes

The gateway accepts the handshake and immediately closes it, rather than refusing at the HTTP layer — a browser cannot read the status of a rejected websocket handshake, so refusing there would leave a client with “it failed” and nothing else.

Code Meaning
4401 ticket missing, malformed, or expired
4403 ticket does not carry the capability this channel needs
4404 no such machine in the ticket’s organization, or no such terminal
4409 machine is not running, or the terminal already has a client
4429 too many sockets for this machine or organization
4503 the control plane could not be reached; retry

4503 is listed separately because it is not a client error. A client that treats an unreachable control plane as a bad ticket will mint a new one and fail the same way forever.

Framing

One rule across every channel, so a client needs one dispatcher:

  • Text frames are JSON control messages, always an object with a type.
  • Binary frames are payload — pixels or pty bytes, never JSON.

Unknown type values are ignored rather than fatal, so a newer gateway can add a message without breaking an older viewer.

The channels

screen

Server to client, capability screen:read. The gateway consumes the guest’s multipart MJPEG stream and republishes each part as one binary frame, so the browser never parses multipart. Each frame is a 20-byte little-endian header — magic CSF1, sequence, geometry, timestamp — followed by the image.

input

Client to server, capability input:write. All control messages, no binary frames: mouse.move, mouse.click, mouse.drag, mouse.scroll, key.type, key.press, key.hotkey, screen.resize.

Every message carries the lease fence. Coordinates are guest pixels, not CSS pixels — the viewer owns the mapping across scaling and letterboxing, because only the viewer knows how it is drawing, and a gateway that guessed would be wrong for every client that draws differently.

Renewal is not on this socket. Renew through the API and, if the fence changed, reconnect the input socket with a fresh ticket. See input control.

terminal

Bidirectional, capability terminal:write. Bytes go both ways unmodified. A terminal that already has a client refuses a second at the handshake with 4409.

events

Server to client. The machine’s event log as it happens, rather than polled.

Through the SDKs

const stream = await machine.screen.stream();
for await (const frame of stream) render(frame.image);
stream.close();
with machine.screen.stream() as stream:
    for frame in stream:
        render(frame.image)

Both mint the ticket, open the socket, decode the header and hand you frames. Screen streams open a session but never take the input lease, so watching a desktop cannot evict whoever is driving it.

In Python this needs the realtime extra: pip install "raster[realtime]". Most callers are agent loops that screenshot and click over ordinary HTTP and never open a socket, so the websocket dependency is opt-in rather than paid for by everybody.

Was this page helpful?