--- title: Connecting your desktop application audience: developers summary: Pair a tablet, push the screen it shows, read the customer's choices, keep the session alive. description: "Integrate a desktop application with the World of Regions customer tablet: pairing, the state channel, the WebSocket transport and the idle clocks." order: 2 updated: "2026-09-14" --- ## Base URL, device identity and polling The production base URL is `https://livestream.world-of-regions.com/api`. Endpoints here use the service's own form, `/Admin/Livestream/`, which the load balancer rewrites `/api/` onto: read `/Admin/Livestream/Connect` as `/api/Connect`. Locally the host is `http://localhost:31337`. - **Bodies** are `application/json` or a multipart form; **responses** are JSON with a top-level `status` of `"OK"` or `"ERR"`, plus `statusMessage` and a machine-readable `code` on failure. Branch on `code`, never on the message. - **Polling is cheap.** GET endpoints return an `ETag`; send it back as `If-None-Match` for a `304 Not Modified` while nothing has changed. Poll every 1–2 seconds. - **`X-Livestream-Device`.** Send no such header on your first request: the response carries one holding a device id the server minted for you. Store it and resend it on every call. The guard counts per device, not per IP — an office shares one public address, and a client that never adopts the id has it blocked. ## Creating a session and pairing a tablet 1. **`POST /Admin/Livestream/Connect`** with an optional `label` and your `config`. You get back a `code` of six upper-case letters (no `I` or `O`), your `config` echoed back canonically, and an `expiresAt`: the code must be claimed within that window, five minutes by default. 2. **The advisor reads the code out** and the customer enters it on the tablet, which claims it with `POST /Admin/Livestream/Claim`. There is no login on the tablet: the code is the credential, and the tablet applies your `config` from the claim response before rendering. 3. **`GET /Admin/Livestream/Status?code=…`** until `connected` turns `true`. It carries the shared `sessionId` that everything afterwards uses; `found:false` means the code is unknown or expired, so mint a fresh one. An unknown or badly typed `config` key never fails the call: it falls back to its default and is listed in `configWarnings`. Connect is refused `409 SESSION_LIMIT_REACHED` when too many livestreams are already running, with `activeSessions` and `maxSessions` in the body. A code counts against that cap from the moment it is minted, so mint one per session. ## Driving the tablet | Method and path | Purpose | |---|---| | `POST /Admin/Livestream/Session/State` | Set the current `view` and `payload`. | | `POST /Admin/Livestream/Session/Config` | Change the config mid-session. | | `GET /Admin/Livestream/Session/Input?sessionId=…` | Read the customer's latest input. | | `POST /Admin/Livestream/Session/Input/Clear` | Clear the customer's selection. | | `GET /Admin/Livestream/Session/Snapshot?sessionId=…` | Rehydrate the whole session. | | `POST /Admin/Livestream/Session/End` | Kill the session. | There is always exactly one selected view; post a new one when it changes: ```json { "sessionId": "073b52…", "view": "results", "payload": {} } ``` `view` is one of `search`, `results`, `details`, `form`, `regionlist`, `standby`, `insuranceSearch`, `insuranceResults`, `insuranceForm`, `parkingSearch`, `parkingResults`, `parkingForm`, `contact`, `content`. The match is case-sensitive; anything else is a `400`. The `payload` overwrites the previous one — the current screen, not a log — so post only when something changes. **Changing the view clears the customer's input**; a same-view update keeps it. Config is session-wide and has its own channel: folded into a `payload`, the next view change would clobber it. Poll `Session/Input` only while the active view is interactive — while its `enableInteractivity` flag is set (`regionList`, `results`, or `reservation` for `form`). > **Never poll `GET Session/State` or `GET Session/Config`.** Both belong to the tablet: a read on > either counts as *tablet* presence and holds a vanished tablet's session open. Your loop is > `Status`, `Session/Input` and `Session/Health`. ## The WebSocket transport The same session is available over one connection instead of those loops; a socket desktop and a polling tablet still drive one session. ``` GET wss:///api/Socket?code= GET wss:///api/Socket?sessionId=&deviceId= ``` Open it on the code before pairing: when the tablet claims, that connection reports the `sessionId` and becomes the session channel. Every frame is a JSON object with a `type`. You receive `hello`, `state`, `pairing`, `ack`, `health`, `ended`, `error` and `pong` — a `state` frame is exactly `GET Session/State`'s body with a `type` added — and you send `state`, `config`, `clearInput`, `activity`, `end`, `health`, `resync` or `ping`, each the twin of one HTTP call. `Connect` and `Claim` stay HTTP, and `deviceId` goes in the query string, since a browser cannot set a header on a handshake. On close, read `event.code`: `4410` (session over, the reason in `event.reason`), `4404` (`SESSION_NOT_FOUND`), `4409` (`CODE_INVALID`), `4400` (`BAD_REQUEST`) and `4429` (`RATE_LIMITED`) are verdicts about the session — act on them and stay off polling. Anything else, `1006` above all, is a transport fault: fall back to polling and retry later. Reconnect with backoff as routine — the platform caps connection lifetime — and the first `state` frame afterwards is the whole picture. ## Keeping the session alive, and ending it Two idle clocks run on every claimed session, and either lapsing ends it. | Clock | Window | Reset by | Not reset by | |---|---|---|---| | Desktop | 15 minutes | a change you push (`Session/State`, `Session/Config`, `Session/Input/Clear`), or `Session/Activity` | your polls, or the health ping | | Tablet | 5 minutes | any call from the tablet, its polls included | — | Polling does not keep a session alive. `GET /Admin/Livestream/Session/Health?sessionId=…` reports whether the session is `alive`, whether the tablet is still there, and how long each side has left (`desktopExpiresInMs`, `tabletExpiresInMs`). Poll it every ten seconds; it extends nothing. When the advisor is present but pushing nothing — a standby screen is pushed once and never changes — send `POST /Admin/Livestream/Session/Activity` with the `sessionId`. Gate it on real interaction (`mousemove`, `keydown`, `click`, the tab becoming visible), throttled to about once a minute, never on a bare timer, which would make every abandoned panel immortal. It answers with the health body, so it can replace that tick. On the socket the `activity` command does the same job: a `ping` does not hold your clock. `POST /Admin/Livestream/Session/End` kills the session and frees its slot at once. After that — or after either clock lapses — every session read returns `{"status":"OK","ended":true,"endedAt":"…","reason":"…"}` instead of data, with `reason` being `OPERATOR`, `IDLE_DESKTOP` or `IDLE_TABLET`. A timed-out session is as terminal as a kill: getting back needs a fresh Connect and Claim. | HTTP | `code` | What to do | |---|---|---| | `400` | `BAD_REQUEST` | A missing `sessionId`, `code` or `view`, or malformed JSON. | | `404` | `SESSION_NOT_FOUND` | A well-formed `sessionId` with no live session. Re-pair. | | `409` | `SESSION_FULL` | A second device tried to claim an already-paired session. | | `429` | `RATE_LIMITED` | Back off for `retryAfterSeconds`. Ordinary polling never trips this. | | `429` | `IP_BLOCKED` | Too many failed attempts from your address; wait `retryAfterSeconds`. | | `503` | `UNAVAILABLE` | The livestream backend is unreachable. Retry with backoff. |