API
REST endpoints, 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, plans, inbox, audit, vault, workflows). - WebSocket for both the daemon's local control surface and the relay-mediated transport between daemons and browser clients.
There is no OpenAPI document today. The canonical wire shapes live in code:
- WebSocket protocol Zod schemas:
viewport/packages/daemon/src/server/ws-protocol.tsand the mirror atplatform/apps/web/src/lib/protocol.ts. A CI job (npm run check:protocol-matrix) catches drift between the two.
Auth model
Two principals talk to the platform: human users via a browser session, and daemons via a per-install issue token.
Browser sessions
The control plane uses WorkOS AuthKit for SSO/login (platform/apps/api/routes/auth.php). Once authenticated, the user gets a Laravel session bridged into the Sanctum guard. All auth:sanctum routes accept this session cookie.
There is no config/sanctum.php override in the repo. Sanctum runs on framework defaults.
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 are Sanctum personal tokens and authorize the same API surface as a session.
REST surface
Grouped by feature. Authentication is auth:sanctum unless noted. {workspace} is a ULID; /api/resources/{workspace}/... is an alias for /api/workspaces/{workspace}/... (via apiResource(parameters: ['resources' => 'workspace'])).
Public
| Method | Path | Purpose |
|---|---|---|
| GET | /api/health | Health probe |
| GET | /api/.well-known/jwks.json | JWKS for relay-token JWT verification |
| POST | /api/integrations/webhooks/{endpoint} | Inbound webhook delivery (HMAC-verified) |
Auth & profile
| Method | Path | Purpose |
|---|---|---|
| GET | /api/auth/login | WorkOS login URL |
| GET | /api/auth/callback | WorkOS callback |
| POST | /api/auth/logout | End the session |
| GET | /api/me | Current user |
| PUT | /api/me | Update profile |
| PUT | /api/me/preferences | Update preferences |
Workspaces & members
| Method | Path | Purpose |
|---|---|---|
| (all) | /api/workspaces (apiResource) | CRUD on workspaces |
| GET / POST / PATCH / DELETE | /api/workspaces/{workspace}/members | Membership |
| POST | /api/workspaces/{workspace}/members/transfer-owner | Transfer ownership |
Pairing
| Method | Path | Auth | Purpose |
|---|---|---|---|
| POST | /api/workspaces/{workspace}/pairing-codes | sanctum | Mint a web-initiated pairing code |
| POST | /api/pairing-codes | throttle:pairing-public | Daemon-initiated pairing code (no auth) |
| POST | /api/pairing-codes/{code}/claim | throttle:pairing-public | Daemon claims a web-initiated code |
| GET | /api/pairing-codes/{code}/status | throttle:pairing-public | Daemon polls for approval |
| POST | /api/pairing-codes/{code}/approve | sanctum | User approves a claimed code |
| POST | /api/pairing-codes/{code}/deny | sanctum | User denies |
Machines & installs
| Method | Path | Purpose |
|---|---|---|
| GET | /api/installs | All installs across the user's workspaces |
| GET | /api/workspaces/{workspace}/installs | Scoped to workspace |
| POST | /api/workspaces/{workspace}/installs/{install}/heartbeat | Daemon liveness |
| DELETE | /api/workspaces/{workspace}/installs/{install} | Revoke pair (the "unpair" surface) |
| GET / DELETE | /api/resources/{workspace}/machines[/{machine}] | Machines (user-owned identities) |
Relay token
| Method | Path | Purpose |
|---|---|---|
| POST | /api/workspaces/{workspace}/relay-token | Browser-side relay JWT |
Plans, inbox, vault, workflows
For the full feature CRUDs, see:
- Plans → REST surface
- Inbox → REST surface
- Context Vault (REST endpoints under
/api/resources/{workspace}/context-vaultsand/api/resources/{workspace}/context-vault/events) - Workflows (REST endpoints under
/api/resources/{workspace}/workflowsand/api/resources/{workspace}/workflow-runs)
Sharing & ACLs
| Method | Path | Purpose |
|---|---|---|
| GET | /api/resources/{workspace}/shared-resources | Resource ACL listing |
| GET | /api/resources/{workspace}/share-principals | Users/teams selectable for sharing |
| POST | /api/resources/{workspace}/shared-resources/{resource}/acl | Share |
| DELETE | /api/resources/{workspace}/shared-resources/{resource}/acl/{aclEntry} | Unshare |
| (CRUD) | /api/resources/{workspace}/share-groups | Named groups (Reviewers, On-call, etc.) |
Audit
| Method | Path | Purpose |
|---|---|---|
| GET | /api/workspaces/{workspace}/audit | Audit feed (filterable; 90-day default retention) |
Runtime (daemon-facing, JWT-authed)
| Method | Path | Purpose |
|---|---|---|
| POST | /api/runtime/relay-token | Daemon obtains a relay JWT |
| POST | /api/runtime/workspaces/{workspace}/daemon-key | Daemon registers its public key |
| POST | /api/runtime/workspaces/{workspace}/agent-hooks/plans | Runtime-side plan-hook ingest |
| GET / POST | /api/runtime/workspaces/{workspace}/context-vaults | Runtime vault read/write |
| POST | /api/runtime/workspaces/{workspace}/context-vault/events/push | Push signed encrypted events (server never decrypts) |
| POST | /api/runtime/workspaces/{workspace}/context-vault/events/pull | Pull signed encrypted events |
| PATCH | /api/runtime/workspaces/{workspace}/workflow-runs/{run}/sync | Daemon → control-plane run sync |
| POST | /api/heartbeat | Daemon health ping (anon, throttled) |
Relay internal
These are called by the relay itself, gated by a shared RELAY_INTERNAL_KEY:
| Method | Path | Purpose |
|---|---|---|
| POST | /api/runtime/internal/relay/validate | Relay validates a JWT before admitting a WS upgrade |
| POST | /api/runtime/internal/relay/presence/upsert | Multi-relay backplane: record presence |
| POST | /api/runtime/internal/relay/presence/resolve | Find which relay holds a given workspace |
| POST | /api/runtime/internal/relay/bus/publish and /pull | Server-mode cross-relay bus |
WebSocket protocol
Daemon ↔ client (incoming, validated by Zod)
Source: ws-protocol.ts, defined as IncomingMessageSchema = z.discriminatedUnion('type', [...]).
type | Required fields |
|---|---|
launch | directoryId, optional prompt, model, thinkingMode, images, configOverrides, requestId |
kill | sessionId |
prompt | sessionId, text, optional images |
respond-permission | sessionId, permissionRequestId, decision.behavior (one of allow, deny, allow-always), optional decision.message |
subscribe | sessionId, optional lastSeq |
unsubscribe | sessionId |
rollback | sessionId, toSha |
branch-retry | sessionId, fromSha |
squash-merge | sessionId, targetBranch, commitMessage |
list-sessions | directoryId, optional limit, offset |
read-session-messages | directoryId, sessionId, optional limit, offset, `delivery: 'ack' |
resume | sessionId, directoryId, optional prompt, model |
watch-discovered-session | sessionId, optional directoryId |
unwatch-discovered-session | same |
sync-request | optional requestId |
workflow-run | directoryId, workflowPath or workflowYaml, optional inputs, runtimeTargetId, platformRunId, rerunOfWorkflowRunId, executionPolicy{mode, branch?}, dataCapturePolicy{transcripts, logs, artifacts} |
workflow-list-runs | optional limit |
workflow-show-run | runId |
workflow-approve | runId, nodeId, approved, optional message, actor |
workflow-cancel | runId, optional message, actor |
supervise | sessionId, active |
respond-hook-permission | hookRequestId, decision.behavior (one of allow, deny), optional decision.message |
Daemon ↔ client (outgoing)
type | Notes |
|---|---|
ack | Reply with `status: ok |
session-update | {sessionId, seq, update: {updateType, ...}} |
session-started | {sessionId, directoryId} |
session-alert | Session-level alert |
session-ended | {sessionId, reason?} |
discovered-sessions-updated | Snapshot of out-of-band agent sessions |
discovered-session-tail | Tail event for a discovered session |
discovered-session-waiting | Discovered session is awaiting input |
workflow-run-updated | Workflow run state update |
hook-session-start / hook-session-end | Claude Code hook bridged |
hook-permission-request | Hook-mediated permission gate |
hook-notification | Hook notification |
hook-tool-completed / hook-tool-failed | Tool lifecycle |
hook-stop | Stop signal |
hook-subagent-start / hook-subagent-stop | Sub-agent lifecycle |
hook-plan-proposed | Plan proposed via the hook bridge |
update.updateType (inside session-update frames)
agent-message, agent-message-chunk, agent-thought-chunk, user-message, tool-call, tool-call-update, permission-request, permission-resolved, state-change, step-committed, token-usage, system-status, attention, streaming-state.
Full per-update Zod schemas: platform/apps/web/src/lib/protocol-types.ts.
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. End-to-end encryption is handled by the daemon-side Noise-IK / Noise-IKpsk2 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.
Protocol-matrix CI
packages/daemon/scripts/check-protocol-matrix.mjs is the cross-package consistency check between ws-protocol.ts and platform/apps/web/src/lib/protocol.ts. Reference: viewport/packages/daemon/docs/protocol-matrix.json. Run via npm run check:protocol-matrix in the daemon package.