Skip to content

Server API Reference

The relay server exposes a REST-like HTTP API used by both the web UI and the CLI. Most endpoints require authentication via an API key (CLI) or session cookie (browser).


Pass your API key in the Authorization header:

Authorization: Bearer pk_live_abc123...

The web UI uses cookie-based sessions managed by better-auth. Log in via POST /api/auth/sign-in/email.


Returns server status. No authentication required.

GET /health

Response:

{ "status": "ok" }

Authentication is handled by better-auth at the /api/auth/* prefix. See the better-auth documentation for the full list of endpoints.

Register a new account or log in to an existing one and issue a CLI API key. Used by pizzapi setup.

Rate-limited: 5 requests per 15 minutes per IP.

Request body:

{
"name": "Jordan", // Required for new accounts; omit for existing
"email": "you@example.com",
"password": "••••••••"
}

Response:

{ "ok": true, "key": "pk_live_abc123..." }

List all registered runner daemons.

Auth: Session cookie.

Response:

[
{
"id": "runner_a1b2c3",
"name": "my-macbook",
"connected": true,
"sessions": ["session_x1y2z3"]
}
]

Spawn a new agent session on a runner.

Auth: API key or session cookie.

Request body:

{
"runnerId": "runner_a1b2c3",
"prompt": "Fix all TypeScript errors",
"cwd": "/path/to/project",
"model": "claude-opus-4-5"
}

Response:

{
"sessionId": "session_x1y2z3",
"shareUrl": "https://relay.example.com/sessions/session_x1y2z3"
}

Restart an agent session on a runner. Sends a restart signal to the runner daemon.

Auth: Session cookie.

Request body:

{
"runnerId": "runner_a1b2c3"
}

Response:

{ "ok": true }

Stop (shut down) a runner daemon.

Auth: Session cookie.

Request body:

{
"runnerId": "runner_a1b2c3"
}

Response:

{ "ok": true }

Register a new terminal (xterm) session on a runner.

Auth: Session cookie.

Request body:

{
"runnerId": "runner_a1b2c3",
"cwd": "/path/to/project", // optional
"cols": 80, // optional, default 80
"rows": 24 // optional, default 24
}

Response:

{ "ok": true, "terminalId": "uuid", "runnerId": "runner_a1b2c3" }

GET /api/runners/{runnerId}/recent-folders

Section titled “GET /api/runners/{runnerId}/recent-folders”

Get recently used working directories for a runner.

Auth: Session cookie.

Response:

{
"folders": ["/Users/jordan/Projects/app", "/Users/jordan/Projects/lib"]
}

DELETE /api/runners/{runnerId}/recent-folders

Section titled “DELETE /api/runners/{runnerId}/recent-folders”

Remove a folder from the recent-folders list.

Auth: Session cookie.

Request body:

{ "path": "/Users/jordan/Projects/old-project" }

Response:

{ "ok": true, "deleted": true }

List available AI models on a runner, excluding user-hidden models.

Auth: Session cookie.

Response:

{
"models": [
{ "provider": "anthropic", "id": "claude-sonnet-4-20250514" },
{ "provider": "openai", "id": "gpt-4o" }
]
}

List registered services and UI panels for a runner.

Auth: Session cookie.

Response:

{
"serviceIds": ["git", "godmother"],
"panels": [{ "id": "panel-1", "title": "Service Panel" }]
}

List available skills on a runner (from Redis cache).

Auth: Session cookie.

Response:

{
"skills": ["deploy", "review-pr", "run-tests"]
}

Get the full content of a specific skill.

Auth: Session cookie.

Response:

{ "name": "deploy", "content": "# Deploy Skill\n..." }

Create a new skill on the runner.

Auth: Session cookie.

Request body:

{ "name": "my-skill", "content": "# Skill instructions..." }

Response:

{ "ok": true, "skills": ["my-skill", "deploy", "review-pr"] }

POST /api/runners/{runnerId}/skills/refresh

Section titled “POST /api/runners/{runnerId}/skills/refresh”

Ask the runner to re-scan skills from disk.

Auth: Session cookie.

Response:

{ "ok": true, "skills": ["deploy", "review-pr"] }

Update an existing skill.

Auth: Session cookie.

Request body:

{ "content": "# Updated skill content..." }

Response:

{ "ok": true, "skills": ["deploy", "review-pr"] }

DELETE /api/runners/{runnerId}/skills/{name}

Section titled “DELETE /api/runners/{runnerId}/skills/{name}”

Delete a skill from the runner.

Auth: Session cookie.

Response:

{ "ok": true, "skills": ["deploy"] }

List files in a directory on the runner. Path must be within the runner’s allowed workspace roots.

Auth: Session cookie.

Request body:

{ "path": "/Users/jordan/Projects/app" }

Response:

{
"ok": true,
"files": [
{ "name": "src", "type": "directory" },
{ "name": "package.json", "type": "file" }
]
}

Search for files by name within a directory on the runner.

Auth: Session cookie.

Request body:

{
"cwd": "/Users/jordan/Projects/app",
"query": "component",
"limit": 100 // optional, default 100
}

Response:

{
"ok": true,
"files": ["src/components/Button.tsx", "src/components/Modal.tsx"]
}

Read the contents of a file on the runner. Supports UTF-8 text and base64-encoded binary.

Auth: Session cookie.

Request body:

{
"path": "/Users/jordan/Projects/app/src/index.ts",
"encoding": "utf8" // "utf8" (default, 512KB max) or "base64" (10MB max)
}

Response:

{ "ok": true, "content": "import express from 'express';\n..." }

Git operations (status, diff, branches, checkout, stage, unstage, commit, push) are handled entirely through the WebSocket service_message channel with serviceId="git", not via REST endpoints. See the WebSocket section below.


GET /api/runners/{runnerId}/sandbox-status

Section titled “GET /api/runners/{runnerId}/sandbox-status”

Get the current sandbox configuration and status for a runner.

Auth: Session cookie.

Response:

{
"ok": true,
"mode": "basic",
"filesystem": { "denyRead": [], "allowWrite": ["/tmp"], "denyWrite": [] },
"network": { "allowedDomains": [], "deniedDomains": [] }
}

PUT /api/runners/{runnerId}/sandbox-config

Section titled “PUT /api/runners/{runnerId}/sandbox-config”

Update the sandbox configuration for a runner.

Auth: Session cookie.

Request body:

{
"mode": "basic", // "none" | "basic" | "full"
"filesystem": {
"denyRead": [],
"allowWrite": ["/tmp", "/Users/jordan/Projects"],
"denyWrite": ["/etc"]
},
"network": {
"allowedDomains": ["api.github.com"],
"deniedDomains": []
}
}

Response:

{ "ok": true }

Get token/cost usage statistics from the runner.

Auth: Session cookie.

Query params:

ParamTypeDefaultDescription
rangestring90dTime range: 7d, 30d, 90d, or all

Response:

{
"ok": true,
"totalInputTokens": 1234567,
"totalOutputTokens": 456789,
"totalCostUsd": 12.34,
"sessions": []
}

List trigger definitions and active listeners for a runner.

Auth: Session cookie.

Response:

{
"triggerDefs": [
{ "type": "github_push", "description": "Fires on git push" }
],
"listeners": [
{ "triggerType": "github_push", "prompt": "Review the push", "cwd": "/project" }
]
}

GET /api/runners/{runnerId}/trigger-listeners

Section titled “GET /api/runners/{runnerId}/trigger-listeners”

List active trigger listeners (auto-spawn subscriptions) for a runner.

Auth: Session cookie.

Response:

{ "listeners": [{ "listenerId": "listener_abc123", "triggerType": "github_push", "prompt": "...", "cwd": "..." }] }

POST /api/runners/{runnerId}/trigger-listeners

Section titled “POST /api/runners/{runnerId}/trigger-listeners”

Add a trigger listener that auto-spawns sessions when a trigger fires.

Auth: Session cookie.

Request body:

{
"triggerType": "github_push",
"prompt": "Review the changes", // optional
"cwd": "/path/to/project", // optional
"model": { "provider": "anthropic", "id": "claude-sonnet-4-20250514" }, // optional
"params": { "branch": "main" } // optional filter params
}

Response:

{ "ok": true, "listenerId": "listener_abc123", "triggerType": "github_push" }

PUT /api/runners/{runnerId}/trigger-listeners/{target}

Section titled “PUT /api/runners/{runnerId}/trigger-listeners/{target}”

Update an existing trigger listener’s configuration. Prefer passing a listenerId as {target} when multiple listeners share the same trigger type. Passing a trigger type is legacy behavior.

Auth: Session cookie.

Request body: Same fields as POST (all optional).

Response:

{ "ok": true, "listenerId": "listener_abc123", "triggerType": "github_push" }

DELETE /api/runners/{runnerId}/trigger-listeners/{target}

Section titled “DELETE /api/runners/{runnerId}/trigger-listeners/{target}”

Remove a trigger listener. Prefer passing a listenerId as {target} when deleting one specific listener. Passing a trigger type is legacy bulk-style behavior.

Auth: Session cookie.

Response:

{ "ok": true, "listenerId": "listener_abc123", "triggerType": "github_push", "removed": 1 }

List available agent definitions on a runner (from Redis cache).

Auth: Session cookie.

Response:

{ "agents": ["reviewer", "deployer"] }

Get the full content of a specific agent definition.

Auth: Session cookie.

Response:

{ "name": "reviewer", "content": "# Agent definition..." }

Create a new agent definition.

Auth: Session cookie.

Request body:

{ "name": "my-agent", "content": "# Agent instructions..." }

Response:

{ "ok": true, "agents": ["my-agent", "reviewer"] }

POST /api/runners/{runnerId}/agents/refresh

Section titled “POST /api/runners/{runnerId}/agents/refresh”

Ask the runner to re-scan agent definitions from disk.

Auth: Session cookie.

Response:

{ "ok": true, "agents": ["reviewer", "deployer"] }

Update an existing agent definition.

Auth: Session cookie.

Request body:

{ "content": "# Updated agent content..." }

Response:

{ "ok": true, "agents": ["reviewer", "deployer"] }

DELETE /api/runners/{runnerId}/agents/{name}

Section titled “DELETE /api/runners/{runnerId}/agents/{name}”

Delete an agent definition.

Auth: Session cookie.

Response:

{ "ok": true, "agents": ["deployer"] }

List Claude Code plugins available on the runner. Optionally pass ?cwd= to scan from a specific directory.

Auth: Session cookie.

Query params:

ParamTypeDescription
cwdstringOptional absolute path to scan for project-local plugins

Response:

{ "plugins": [{ "name": "my-plugin", "path": "/path/to/plugin" }] }

POST /api/runners/{runnerId}/plugins/refresh

Section titled “POST /api/runners/{runnerId}/plugins/refresh”

Ask the runner to re-scan plugins from disk.

Auth: Session cookie.

Response:

{ "ok": true, "plugins": [] }

List all sessions for the authenticated user, including both live (in-memory) and persisted sessions.

Auth: Session cookie.

Query params:

ParamTypeDefaultDescription
includePersistedstringtrueSet to false or 0 to exclude persisted (historical) sessions

Response:

{
"sessions": [
{
"sessionId": "session_x1y2z3",
"runnerId": "runner_a1b2c3",
"status": "running"
}
],
"persistedSessions": []
}

List pinned sessions for the authenticated user.

Auth: Session cookie.

Response:

{ "pinnedSessions": [{ "sessionId": "session_x1y2z3", "isPinned": true }] }

Pin a session so it persists in the session list.

Auth: Session cookie.

Response:

{ "ok": true, "isPinned": true }

Unpin a session.

Auth: Session cookie.

Response:

{ "ok": true, "isPinned": false }

POST /api/sessions/{sessionId}/attachments

Section titled “POST /api/sessions/{sessionId}/attachments”

Upload file attachments to a session (e.g. for the agent to process). Accepts one or more files.

Auth: Session cookie.

Content-Type: multipart/form-data

Form fields: file or files (one or more File entries).

Max file size: Configured by the server (default: 10 MB).

Response:

{
"attachments": [
{
"attachmentId": "att_uuid",
"filename": "screenshot.png",
"mimeType": "image/png",
"size": 54321,
"expiresAt": "2025-01-15T12:00:00Z"
}
]
}

Download a previously uploaded attachment. Returns the file with appropriate content headers.

Auth: Session cookie or API key (via x-api-key header or ?apiKey= query param).

Response: Binary file with headers:

  • Content-Type — the attachment’s MIME type
  • Content-Dispositioninline; filename="..."
  • X-Attachment-Id — the attachment ID
  • X-Attachment-Filename — percent-encoded original filename

Fire a trigger into a connected session. Used by external integrations to send events to running agents.

Auth: API key (x-api-key header) or session cookie.

Request body:

{
"type": "webhook", // trigger type identifier
"payload": { "repo": "my/repo" }, // arbitrary payload object
"deliverAs": "steer", // optional: "steer" (default, interrupts) or "followUp" (queues)
"expectsResponse": false, // optional, default false
"source": "github", // optional source identifier
"summary": "New push to main" // optional human-readable summary
}

Response:

{ "ok": true, "triggerId": "ext_abc123" }

List recent trigger history for a session.

Auth: API key or session cookie.

Query params:

ParamTypeDefaultDescription
limitnumber50Max triggers to return (capped at 200)

Response:

{
"triggers": [
{
"triggerId": "ext_abc123",
"type": "webhook",
"source": "external:github",
"summary": "New push to main",
"payload": {},
"deliverAs": "steer",
"ts": "2025-01-15T12:00:00Z",
"direction": "inbound"
}
]
}

Clear trigger history for a session.

Auth: API key or session cookie.

Response:

{ "ok": true }

GET /api/sessions/{sessionId}/available-triggers

Section titled “GET /api/sessions/{sessionId}/available-triggers”

List trigger types available on the session’s runner (from runner service catalog).

Auth: API key or session cookie.

Response:

{
"triggerDefs": [
{
"type": "github_push",
"description": "Fires on git push events",
"params": [{ "name": "branch", "type": "string" }],
"schema": {}
}
]
}

GET /api/sessions/{sessionId}/trigger-subscriptions

Section titled “GET /api/sessions/{sessionId}/trigger-subscriptions”

List active trigger subscriptions for a session.

Auth: API key or session cookie.

Response:

{
"subscriptions": [
{ "subscriptionId": "sub_abc123", "triggerType": "github_push", "runnerId": "runner_a1b2c3" }
]
}

POST /api/sessions/{sessionId}/trigger-subscriptions

Section titled “POST /api/sessions/{sessionId}/trigger-subscriptions”

Subscribe a session to a trigger type. The trigger type must be available on the session’s runner.

Auth: API key or session cookie.

Request body:

{
"triggerType": "github_push",
"params": { "branch": "main" }, // optional, validated against trigger def
"filters": [ // optional output filters
{ "field": "repo", "value": "my/repo", "op": "eq" }
],
"filterMode": "and" // optional: "and" (default) or "or"
}

Response:

{
"ok": true,
"subscriptionId": "sub_abc123",
"triggerType": "github_push",
"runnerId": "runner_a1b2c3"
}

PUT /api/sessions/{sessionId}/trigger-subscriptions/{target}

Section titled “PUT /api/sessions/{sessionId}/trigger-subscriptions/{target}”

Update params/filters on an existing trigger subscription. Prefer passing a subscriptionId as {target} or via ?subscriptionId= when multiple same-type subscriptions exist. Passing only a trigger type is legacy behavior.

Auth: API key or session cookie.

Request body:

{
"params": { "branch": "develop" },
"filters": [{ "field": "author", "value": "bot", "op": "contains" }],
"filterMode": "and"
}

Response:

{ "ok": true, "subscriptionId": "sub_abc123", "triggerType": "github_push", "runnerId": "runner_a1b2c3" }

DELETE /api/sessions/{sessionId}/trigger-subscriptions/{target}

Section titled “DELETE /api/sessions/{sessionId}/trigger-subscriptions/{target}”

Unsubscribe a session from a trigger subscription. Prefer passing a subscriptionId as {target} or via ?subscriptionId= when deleting one specific subscription. Passing only a trigger type is legacy bulk-style behavior.

Auth: API key or session cookie.

Response:

{ "ok": true, "subscriptionId": "sub_abc123", "triggerType": "github_push", "removed": 1 }

POST /api/runners/{runnerId}/trigger-broadcast

Section titled “POST /api/runners/{runnerId}/trigger-broadcast”

Broadcast a trigger to all sessions subscribed to a trigger type on this runner. Intended for runner services.

Auth: API key only (x-api-key header).

Request body:

{
"type": "github_push",
"payload": { "repo": "my/repo", "branch": "main" },
"deliverAs": "steer", // optional: "steer" or "followUp"
"source": "github", // optional
"summary": "Push to main" // optional
}

Response:

{
"ok": true,
"delivered": 3, // number of existing sessions that received the trigger
"spawned": 1, // number of auto-spawned sessions from listeners
"triggerId": "ext_abc123"
}

Webhooks allow external services to spawn sessions and fire triggers via HMAC-authenticated HTTP requests.

Create a new webhook.

Auth: Session cookie.

Request body:

{
"name": "Deploy on push",
"source": "github",
"runnerId": "runner_a1b2c3", // optional — runner to spawn sessions on
"cwd": "/path/to/project", // optional
"prompt": "Review the changes", // optional — initial prompt for spawned sessions
"model": { "provider": "anthropic", "id": "claude-sonnet-4-20250514" }, // optional
"eventFilter": ["push", "pull_request"] // optional — only fire for these event types
}

Response (201):

{
"webhook": {
"id": "wh_uuid",
"name": "Deploy on push",
"source": "github",
"secret": "whsec_...",
"enabled": true,
"runnerId": "runner_a1b2c3"
}
}

List all webhooks for the authenticated user.

Auth: Session cookie.

Response:

{ "webhooks": [{ "id": "wh_uuid", "name": "Deploy on push", "source": "github", "enabled": true }] }

Get details of a specific webhook.

Auth: Session cookie.

Response:

{ "webhook": { "id": "wh_uuid", "name": "...", "source": "...", "secret": "whsec_...", "enabled": true } }

Update a webhook’s configuration.

Auth: Session cookie.

Request body: Any subset of the creation fields (name, source, runnerId, cwd, prompt, model, eventFilter, enabled).

Response:

{ "webhook": { "id": "wh_uuid", "name": "Updated name", "enabled": true } }

Delete a webhook.

Auth: Session cookie.

Response:

{ "ok": true }

Fire a webhook — spawns a new session and delivers the payload as a trigger. No session cookie required — authenticated via HMAC signature.

Auth: HMAC-SHA256 signature of the raw request body using the webhook’s secret, sent in the X-Webhook-Signature header.

Request body: Any JSON object. If the body contains a type field, it’s checked against the webhook’s eventFilter.

Headers:

X-Webhook-Signature: <hex-encoded HMAC-SHA256>
Content-Type: application/json

Response:

{ "ok": true, "triggerId": "wh_abc123", "sessionId": "session_x1y2z3" }

If the event type is filtered out:

{ "ok": true, "filtered": true }

Get the VAPID public key for Web Push subscription. No authentication required.

Response:

{ "publicKey": "BNx..." }

Subscribe the browser to push notifications.

Auth: Session cookie.

Request body:

{
"endpoint": "https://fcm.googleapis.com/fcm/send/...",
"keys": {
"p256dh": "base64-encoded-key",
"auth": "base64-encoded-auth"
},
"enabledEvents": "question,error" // optional comma-separated event list
}

Response:

{ "ok": true, "id": "sub_uuid" }

Unsubscribe the browser from push notifications.

Auth: Session cookie.

Request body:

{ "endpoint": "https://fcm.googleapis.com/fcm/send/..." }

Response:

{ "ok": true }

List push subscriptions for the authenticated user.

Auth: Session cookie.

Response:

{
"subscriptions": [
{
"id": "sub_uuid",
"endpoint": "https://fcm.googleapis.com/...",
"createdAt": "2025-01-15T12:00:00Z",
"enabledEvents": "question,error",
"suppressChildNotifications": false
}
]
}

Update which events trigger push notifications for a subscription.

Auth: Session cookie.

Request body:

{
"endpoint": "https://fcm.googleapis.com/...",
"enabledEvents": "question,error,completion"
}

Response:

{ "ok": true }

Control whether child (sub-agent) sessions trigger push notifications.

Auth: Session cookie.

Request body:

{
"endpoint": "https://fcm.googleapis.com/...",
"suppress": true
}

Response:

{ "ok": true }

Answer a pending agent question directly from a push notification (e.g. from mobile).

Auth: Session cookie.

Request body:

{
"sessionId": "session_x1y2z3",
"toolCallId": "tc_abc123",
"text": "Yes, proceed with the deployment"
}

Response:

{ "ok": true }

Error codes:

  • 409 — No question pending, or answer already submitted
  • 403 — Session not in collab mode
  • 502 — Runner not connected

Get the list of hidden (disabled) models for the authenticated user.

Auth: Session cookie.

Response:

{ "hiddenModels": ["openai/gpt-4", "anthropic/claude-2"] }

Update the list of hidden models.

Auth: Session cookie.

Request body:

{ "hiddenModels": ["openai/gpt-4"] }

Response:

{ "ok": true, "hiddenModels": ["openai/gpt-4"] }

The tunnel endpoints proxy HTTP requests to services running on the runner’s localhost. This allows the web UI to access dev servers, databases, or any local service through the relay.

Session-based tunnel proxy. All HTTP methods are supported (GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS).

Auth: Session cookie (must be the session owner).

The request is forwarded to 127.0.0.1:{port} on the runner. HTML, JS, and CSS responses are rewritten to route absolute paths through the tunnel prefix. Auth headers (Cookie, Authorization, x-api-key) are stripped before forwarding.


Runner-based tunnel proxy (preferred). Same as above but addressed by runner ID instead of session ID, making the URL stable across session restarts.

Auth: Session cookie (must own the runner).