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

# Live view, VNC and recordings

Request a short-lived viewer token to watch and control a session through a browser. The token grants access to one session and can be used in its viewer URL or stream connection. Treat it as a credential and keep the account API key on your server.

## POST /v1/session/\{session\_id}/view-token

Response (200):

```json theme={"dark"}
{"token": "<token>", "expires_in": 1800, "live_view_url": "/v1/session/sess_3d245e56b86d4bc8ae32095b644117d4/live-view?token=<token>"}
```

The token lasts 1800 seconds and is reusable until then. `live_view_url` is a path: prefix `https://back.tilion.dev`. The same token opens the WebSocket streams: `wss://back.tilion.dev/v1/session/{id}/live?token=<token>` and `wss://back.tilion.dev/v1/session/{id}/webrtc?token=<token>`. Errors: 404 or 410 for an unknown or ended session; 503 `view tokens are not available on this deployment`.

## GET /v1/session/\{session\_id}/live-view

A ready-made interactive viewer page (`text/html`). Auth: `?token=` from `/view-token` (no Bearer). Without a token: 401 `missing view token; mint one with POST /v1/session/{id}/view-token`. With a bad token: 403 `invalid or expired view token`. Those two answers come before any session lookup, so the status never reveals whether an id exists; an ended session with a valid token answers 410.

## WS /v1/session/\{session\_id}/live

JPEG screencast of the page, interactive. URL: `wss://back.tilion.dev/v1/session/{session_id}/live?token=<token>` with the token from `/view-token`. The stream is a viewer: it does not hold the browser, so REST drive calls and `fetch` proceed normally while it is attached.

What a client receives: text frames, each one base64 of a JPEG of the current page at the viewport size, delivered continuously at about 2 frames per second; consecutive frames can be byte-identical on a static page, so de-duplicate client side if you are storing them. Frames carry no timestamp; stamp them on receipt. The stream ends when the session ends.

What a client sends, as JSON text frames:

* `{"t": "a"}` after rendering a frame. Once you have sent one ack, frames are paced to your acks, so a slow viewer gets the newest frame instead of a growing backlog. A client that never acks gets every frame.
* Mouse: `{"t": "m", "type": "mousePressed" | "mouseReleased" | "mouseMoved" | "mouseWheel", "x": 120, "y": 80, "button": "left", "clickCount": 1, "dx": 0, "dy": 120}` (page coordinates; `button` and `clickCount` for press and release; `dx`, `dy` for wheel).
* Keyboard: `{"t": "k", "type": "keyDown" | "keyUp" | "char", "key": "a", "code": "KeyA", "text": "a", "keyCode": 65}`.

Input lands on the tab you are watching as real user input. Every view token allows input; there is no read-only token mode. A bad token is refused at the handshake with HTTP 403 and `{"detail": {"error": "invalid_token", "message": "missing, expired or invalid token for this session; mint a new one"}}`; an ended session is refused with its 410. The bytes streamed are billed as egress.

## WS /v1/session/\{session\_id}/webrtc

A smooth video of the session over WebRTC, for viewers that can take it; the JPEG `/live` socket is the fallback. Auth: `?token=`. The first message after the handshake:

```json theme={"dark"}
{"type": "ice", "iceServers": [{"urls": "stun:stun.l.google.com:19302"}, {"urls": ["turn:turn.tilion.dev:3478?transport=udp", "turn:turn.tilion.dev:3478?transport=tcp", "turns:turn.tilion.dev:443?transport=tcp"], "username": "...", "credential": "..."}], "relayOnly": false}
```

Build an `RTCPeerConnection` with those `iceServers` (the TURN credential is short-lived and minted per connection), send `{"type": "offer", "sdp": "..."}` and receive `{"type": "answer", "sdp": "..."}`; the video track then flows peer to peer, and input rides this socket with the same `{"t": "m" ...}` and `{"t": "k" ...}` events as `/live`. When HD streaming is not available for this session right now the socket closes with code 1013 right after the handshake: keep the `/live` view you already have. Video bytes are billed as egress.

## POST /v1/session/\{session\_id}/vnc-token

For a `full_browser: true` session: a token and URL to watch and drive the whole Chrome window (noVNC), not just the page.

Response (200):

```json theme={"dark"}
{
  "token": "<token>",
  "expires_in": 1800,
  "vnc_view_url": "/v1/session/sess_3d245e2a0c1c499c9955f185b6acf251/vnc/vnc_lite.html?token=<token>&path=v1%2Fsession%2Fsess_3d245e2a0c1c499c9955f185b6acf251%2Fvnc-ws%3Ftoken%3D<token>&scale=true&reconnect=true"
}
```

Open `https://back.tilion.dev` plus `vnc_view_url` in a browser or an iframe; it loads the viewer and connects it to the window. Errors: 404 `full-browser (noVNC) view is not enabled on this deployment`; 404 or 410 for the session; 503 when the token cannot be minted.

## GET /v1/session/\{session\_id}/vnc/\{path}

The noVNC viewer and its assets, served for one session. Auth: `?token=` from `/vnc-token` on the first load, which also sets a cookie scoped to this path so the viewer's own module imports work; without either, 403 `invalid or missing view token`. Directories are not listed. Assets are billed as egress.

## WS /v1/session/\{session\_id}/vnc-ws

The VNC transport the viewer uses; binary frames (RFB). Auth: `?token=` from `/vnc-token`. An attached VNC viewer holds the browser: REST drive calls answer 409 `session_driven_by_client` until it disconnects. Bytes are billed as egress.

## Recordings

Set `record: true` on session creation to capture a frame replay. `record: false` disables capture; null uses the deployment default. A successful recording request returns `replay_url`, a tokenized path to an HTML player. Prefix it with `https://back.tilion.dev`. The player displays captured frames; the replay URL itself is not a video file.

Capture stops at the plan's recorded-minute cap while the browser session can continue:

| Plan | Captured minutes per session |
| - | - |
| Free | 3 |
| Developer | 5 |
| Team | 100 |
| Enterprise | 500 |

### GET /v1/recordings

Auth: Bearer API key. Returns `{"recordings": [...]}`, most recent first, up to 100 rows. Add `?session_id=<id>` to filter. Each row carries `session_id`, `rec_id`, `status`, `frame_count`, `duration_s`, `bytes`, `tier`, `cap_minutes`, `created_at` and `expires_at`. Counts may not be final while capture is running.

### GET /v1/recordings/\{session\_id}/\{rec\_id}/view

Auth: Bearer API key. Mint recording-scoped URLs:

| Field | Meaning |
| - | - |
| `replay_url` | HTML frame player. |
| `manifest_url` | JSON frame list and timing metadata. |
| `download_url` | ZIP bundle of frames, manifest and player. |
| `mp4_url` | MP4 export request URL, subject to encoder availability. |
| `expires_in` | URL lifetime in seconds. |

Prefix each returned path with `https://back.tilion.dev` and use it as returned, including its token. Mint fresh URLs when they expire. Unknown or inaccessible recordings answer 404.

### GET /v1/recordings/\{session\_id}/\{rec\_id}/download

Use the tokenized `download_url` from `/view`. The default format is ZIP (`application/zip`), including captured frames, `manifest.json` and `replay.html`. A running recording can be downloaded with the frames captured so far. Close the session to finalize its recording.

`format=mp4` requests H.264 export. While recording is running it answers 409 `recording_in_progress`. After close, an available encoder can return 202 `{"status": "processing"}` while encoding; retry the same URL until it returns `video/mp4`. If the encoder is unavailable, the route answers 503 `encoder not available`; use the ZIP or replay player. Invalid or expired authorization answers 403; a missing recording answers 404.

### DELETE /v1/recordings/\{session\_id}/\{rec\_id}

Auth: Bearer API key. Delete the recording; success is `204 No Content`.


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