API
The generated REST reference, the WebSocket protocol, and the auth model that ties them together.
Viewport exposes two protocol surfaces:
- REST at
https://api.getviewport.comfor control-plane reads and writes (workspaces, machines, pairing, runs, receipts, plans, inbox, audit, vault, workflows). - WebSocket for both the daemon's local control surface and the relay-mediated transport between daemons and browser clients.
The REST reference
The pages in this section are generated from the platform's OpenAPI 3.1
document (viewportai/platform: apps/api/openapi.json) — the same spec the
platform's contract tests run against. One page per resource group; use the
sidebar or jump to the busiest groups:
- Workspaces · Resources (workspace-scoped aliases)
- Workflow Runs · Runs · Sessions
- Pairing Codes · Runtime (daemon-facing)
- Billing · Insights · Audit
Honesty note on coverage. The spec is contract-tested but unevenly
hand-tightened: operations marked generated contract stub are real, served
routes whose request/response schemas are still placeholders
(GenericApiResponse). The per-page banner tells you how much of each group is
stubbed. Treat non-stub schemas as reliable; treat stubs as "the route exists,
verify the shape against a live response".
The snapshot this reference was generated from is committed at
openapi/openapi.json,
with its digests in openapi/openapi.meta.json. CI fails when the published
reference lags the platform's spec — see "Keeping this current" below.
Auth model
Two principals talk to the platform: human users via the web app, and daemons via a per-install issue token.
Browser sessions
The server API uses WorkOS AuthKit for SSO/login. In the current hosted app, the callback returns a bearer token to the browser app, and authenticated REST routes accept that token. This is an alpha implementation detail and may move to a cookie-backed session before GA.
Daemon → platform
Daemons pair via the pairing flow and receive a per-install issue token (bcrypt-hashed in installs.daemon_issue_token_hash). The daemon presents this token to runtime endpoints (/api/runtime/*, throttled). The token grants a relay JWT via POST /api/runtime/relay-token, which the daemon then presents on its WebSocket upgrade to the relay.
Personal access tokens
Users can mint personal API tokens at /settings/api-tokens:
| Method | Path | Purpose |
|---|---|---|
| GET | /api/me/api-tokens | List |
| POST | /api/me/api-tokens | Create (returns plaintext once) |
| DELETE | /api/me/api-tokens/{tokenId} | Revoke |
These tokens authorize the same API surface as a browser session.
Conventions
{workspace}is a ULID;/api/resources/{workspace}/...is an alias for/api/workspaces/{workspace}/...— the Resources group mirrors workspace-scoped routes.- Authentication is the browser/user token (
sanctum) unless a route is under/api/runtime/*(daemon issue token / JWT) or explicitly public (/api/health,/api/.well-known/*, webhook ingestion).
WebSocket protocol
The daemon ↔ client wire shapes live in the open-source runtime code as Zod schemas:
viewport/packages/daemon/src/server/ws-protocol.ts
(IncomingMessageSchema, a discriminated union on type: launch, kill,
prompt, respond-permission, subscribe, workflow-run,
workflow-approve, and the rest). Outgoing frames are ack,
session-update, session-started/session-ended, discovered-session
events, workflow-run-updated, and the Claude Code hook bridge
(hook-*). A CI job catches drift between the daemon and web client copies
(npm run check:protocol-matrix in the daemon package, reference
viewport/packages/daemon/docs/protocol-matrix.json).
Relay WS upgrade
GET wss://relay.getviewport.com/ws
?role=workspace-daemon|client
&workspaceId=ULID
&runtimeTargetId=ULID # optional, scopes to one machineAuth: a JWT in Authorization: Bearer …, the Sec-WebSocket-Protocol subprotocol header, or ?token=. The relay calls POST /api/runtime/internal/relay/validate to admit the upgrade.
The relay does not decrypt frames. It identifies envelopes (workspace, runtime target) and routes daemon ↔ client. Payload encryption is handled by the daemon/browser relay session inside the envelope.
Pairing flow (end-to-end)
┌──────────┐ 1. POST /api/workspaces/{ws}/pairing-codes ┌────────────┐
│ Browser ├────────────────────────────────────────────────────► │
│ │◄───────────────────────── code ─────────────────────│ Platform │
└────┬─────┘ │ API │
│ 2. shows code └─────┬──────┘
│ │
┌────▼────┐ 3. POST /api/pairing-codes/{code}/claim │
│ Daemon ├─────────────────────────────────────────────────────────────►
│ vpd pair│◄──────────────────── status_token ──────────────────────────
│ <code> │
│ │ 4. GET /api/pairing-codes/{code}/status (poll, with token)
│ ├─────────────────────────────────────────────────────────────►
└─────────┘
▲ │
│ 5. User clicks Approve in browser │
│ ┌───────────────────────────────────────────────────┘
│ │ POST /api/pairing-codes/{code}/approve
│ ▼
│ The platform binds machine → workspace, retires any
│ prior install for that pair, and issues a fresh relay
│ credential for the daemon (token hash stored, plaintext
│ returned once in the approve response).
│
│ 6. status returns approved + relay_credentials
└─── 7. daemon writes config, restartDaemon() ────► connects to relaySame flow for daemon-initiated pairing, just swap steps 1 and 3.
Keeping this current
The platform repo owns the spec. This repo commits a snapshot and generates the reference pages from it:
# in the docs repo, with the platform checkout as a sibling directory:
npm run api:sync # copy platform/apps/api/openapi.json + regenerate pages
npm run api:generate # regenerate pages from the committed snapshot only
npm run api:check # what CI runs — fails if pages lag the snapshotCI enforces two layers: the committed pages must match the committed snapshot
(always), and the snapshot's git blob SHA must match
viewportai/platform:apps/api/openapi.json on main (when the
PLATFORM_READ_TOKEN secret is available to the workflow).