relay

Documentation

HTTP API

Every control-plane endpoint, with the auth model and curl examples. The SDK is a thin wrapper around this — you can call it directly from any language.

Authentication

All /v1/* routes require a Bearer token — your tenant's relay_live_… key from pnpm bootstrap.

-H "authorization: Bearer relay_live_…"

The single /internal/* route requires Internal $RELAY_INTERNAL_SECRET when the secret is configured — it's how the runtime calls back to the control plane for custom tool results. Don't expose this from your client.

Endpoints at a glance

MethodPathPurpose
GET/healthpublic liveness
POST/v1/signupcreate a tenant + mint the first API key (no auth)
POST/v1/runsstart a run; returns an SSE event stream
GET/v1/runslist runs (filters: status, roots, workflow)
GET/v1/runs/:idrun metadata
GET/v1/runs/:id/eventsfull event log (ordered)
POST/v1/runs/:id/tool-results/:toolUseIdSDK posts a custom tool output
GET/v1/workflows/:idfull run tree + aggregated cost for a workflow
POST/v1/keysmint a new API key (for rotation)
GET/v1/keyslist this tenant's keys (no secrets)
DELETE/v1/keys/:idrevoke a key
GET/v1/auditsecurity-relevant audit log for this tenant
PUT/v1/credentials/:providerupload or rotate provider key
GET/v1/credentialslist (no secrets returned)
DELETE/v1/credentials/:providerrevoke
GET/v1/memories?namespace=&limit=list memories
DELETE/v1/memories/:iddelete one
DELETE/v1/memories?namespace=clear a namespace
POST/v1/transcribeaudio → text (Whisper, multipart upload)
POST/v1/synthesizetext → audio stream (OpenAI TTS)
GET/internal/runs/:id/tool-result/:toolUseIdruntime long-poll (internal-auth)

POST /v1/signup

Create a tenant and mint its first API key. The only unauthenticated mutation in the API — protect with a captcha at the edge in production if you allow self-service signup.

curl -X POST $RELAY_URL/v1/signup \
  -H "content-type: application/json" \
  -d '{
    "email": "you@example.com",
    "openaiApiKey": "sk-...",
    "anthropicApiKey": "sk-ant-..."
  }'

# Response:
# {
#   "apiKey": "relay_live_<long-secret>",   // shown once, NEVER returned again
#   "tenant": { "id": "uuid", "name": "you@example.com" },
#   "providers": ["openai", "anthropic"],
#   "email": { "sent": true, "reason": null }
# }

POST /v1/runs

Start an agent run. Returns an SSE stream of events.

curl -N -X POST $RELAY_URL/v1/runs \
  -H "authorization: Bearer $RELAY_API_KEY" \
  -H "content-type: application/json" \
  -d '{
    "model": "gpt-4o-mini",
    "system": "You are a helpful assistant.",
    "input": "What is 23 * 47?",
    "tools": [{ "name": "calculator", "kind": "builtin" }],
    "memory": { "namespace": "demo-user" }
  }'

Response: text/event-stream

Every event is one SSE frame. Frames look like:

data: {"type":"token","text":"23"}

data: {"type":"token","text":" *"}

data: {"type":"tool_call","id":"call_…","name":"calculator","input":{"a":23,"b":47,"op":"*"}}

data: {"type":"tool_result","id":"call_…","output":1081}

data: {"type":"done","output":"…","usage":{"input_tokens":234,"output_tokens":12}}

The response header x-run-id carries the run id — you need it to post tool results.

Errors

StatusCause
400missing model/input; unknown model family; no credential for that provider
401missing or invalid bearer token
502runtime rejected the run (network, provider error, etc.)

POST /v1/runs/:id/tool-results/:toolUseId

Used by the SDK when a custom tool fires. You can hit it directly if you're implementing your own SDK.

curl -X POST $RELAY_URL/v1/runs/$RUN_ID/tool-results/$TOOL_USE_ID \
  -H "authorization: Bearer $RELAY_API_KEY" \
  -H "content-type: application/json" \
  -d '{ "output": { "ok": true, "user": { "id": "u_001" } } }'

Credentials

# upload / rotate
curl -X PUT $RELAY_URL/v1/credentials/openai \
  -H "authorization: Bearer $RELAY_API_KEY" \
  -H "content-type: application/json" \
  -d '{"apiKey":"sk-...","label":"prod","baseUrl":"..."}'   # baseUrl optional

# list (apiKey field is never returned)
curl -H "authorization: Bearer $RELAY_API_KEY" $RELAY_URL/v1/credentials

# revoke
curl -X DELETE -H "authorization: Bearer $RELAY_API_KEY" \
  $RELAY_URL/v1/credentials/openai

Memories

curl -H "authorization: Bearer $RELAY_API_KEY" \
  "$RELAY_URL/v1/memories?namespace=user:42&limit=50"

curl -X DELETE -H "authorization: Bearer $RELAY_API_KEY" \
  "$RELAY_URL/v1/memories/<memory-id>"

curl -X DELETE -H "authorization: Bearer $RELAY_API_KEY" \
  "$RELAY_URL/v1/memories?namespace=user:42"

List & inspect runs

# list runs (paginated by ?limit, filtered by ?status)
curl -H "authorization: Bearer $RELAY_API_KEY" \
  "$RELAY_URL/v1/runs?status=completed&limit=20"

# only top-level (root) runs — one row per workflow
curl -H "authorization: Bearer $RELAY_API_KEY" \
  "$RELAY_URL/v1/runs?roots=true"

# all runs in a specific workflow (filter on workflow_id)
curl -H "authorization: Bearer $RELAY_API_KEY" \
  "$RELAY_URL/v1/runs?workflow=<workflow-id>"

# run metadata
curl -H "authorization: Bearer $RELAY_API_KEY" \
  "$RELAY_URL/v1/runs/<run-id>"

# full event log
curl -H "authorization: Bearer $RELAY_API_KEY" \
  "$RELAY_URL/v1/runs/<run-id>/events"

GET /v1/workflows/:id

Returns the entire tree of runs that share a workflow_id (sub-agents, Graph steps), ordered depth-first for indented rendering, plus aggregated cost across the whole tree.

curl -H "authorization: Bearer $RELAY_API_KEY" \
  "$RELAY_URL/v1/workflows/<workflow-id>"

# Response:
# {
#   "workflowId": "uuid",
#   "runs": [
#     { "id": "...", "depth": 0, "parentRunId": null, "model": "...", ... },
#     { "id": "...", "depth": 1, "parentRunId": "...", "model": "...", ... },
#     { "id": "...", "depth": 2, "parentRunId": "...", "model": "...", ... }
#   ],
#   "cost": {
#     "workflowId": "uuid",
#     "runCount": 3,
#     "inputTokens": 1240,
#     "outputTokens": 488
#   }
# }

See Workflows for how trees are formed (subagent + Graph).

API keys

Zero-downtime rotation: mint a new one, verify, revoke the old.

# mint a new key (current key authorizes; new key returned ONCE)
curl -X POST $RELAY_URL/v1/keys \
  -H "authorization: Bearer $OLD_KEY" \
  -H "content-type: application/json" \
  -d '{"name":"rotated 2026-05"}'
# → { "apiKey": "relay_live_...NEW", "descriptor": { "id": "...", "prefix": "..." } }

# list (no secrets — only id, prefix, name, timestamps, revokedAt)
curl -H "authorization: Bearer $RELAY_API_KEY" $RELAY_URL/v1/keys

# revoke a specific key
curl -X DELETE -H "authorization: Bearer $RELAY_API_KEY" \
  $RELAY_URL/v1/keys/<key-id>

# Self-revocation is refused unless you pass ?force=true (prevents
# accidentally locking yourself out — the secret can't be recovered).
curl -X DELETE -H "authorization: Bearer $RELAY_API_KEY" \
  "$RELAY_URL/v1/keys/<calling-key-id>?force=true"

GET /v1/audit

Security-relevant actions are logged per tenant. The log is RLS-protected — a tenant can only see their own rows.

# recent events
curl -H "authorization: Bearer $RELAY_API_KEY" \
  "$RELAY_URL/v1/audit?limit=100"

# filter by action
curl -H "authorization: Bearer $RELAY_API_KEY" \
  "$RELAY_URL/v1/audit?action=api_key.created"

# paginate backward by createdAt
curl -H "authorization: Bearer $RELAY_API_KEY" \
  "$RELAY_URL/v1/audit?before=2026-05-26T00:00:00Z&limit=200"

# Event shape:
# {
#   "id": "uuid",
#   "tenantId": "uuid",
#   "actor": "api_key:<keyId>" | "signup" | "admin" | "system",
#   "action": "api_key.created" | "credential.updated" | "master_key.rotated" | ...,
#   "targetType": "api_key" | "provider_credentials" | ...,
#   "targetId": "...",
#   "metadata": { ... },
#   "ipAddress": "x.x.x.x" | null,
#   "userAgent": "..." | null,
#   "createdAt": "ISO-8601"
# }

See Security → Audit log for the full list of recorded actions.

Voice — transcribe + synthesize

Both endpoints use the tenant's existing OpenAI BYOK credential. Audio bytes pass through to OpenAI and back — never persisted on the Relay side.

# Speech → text (Whisper)
curl -X POST $RELAY_URL/v1/transcribe \
  -H "authorization: Bearer $RELAY_API_KEY" \
  -F file=@clip.mp3 \
  -F language=es
# → {"text": "Hola, esto es una prueba."}

# Text → speech (OpenAI TTS, streams audio bytes)
curl -X POST $RELAY_URL/v1/synthesize \
  -H "authorization: Bearer $RELAY_API_KEY" \
  -H "content-type: application/json" \
  -d '{
    "input": "Hola desde Relay",
    "model": "tts-1",
    "voice": "nova",
    "format": "mp3"
  }' \
  --output hello.mp3

Full reference (voices, models, formats, limits) on the Voice page.

Parent / workflow params on POST /v1/runs

To link a run as a child of another (for sub-agent / graph composition without using the SDK helpers), pass parentRunId or workflowId on the body. The server validates the parent belongs to the same tenant.

curl -N -X POST $RELAY_URL/v1/runs \
  -H "authorization: Bearer $RELAY_API_KEY" \
  -H "content-type: application/json" \
  -d '{
    "model": "gpt-4o-mini",
    "input": "do the thing",
    "parentRunId": "<parent-uuid>",        # one-hop parent
    "workflowId":  "<workflow-root-uuid>"  # shared across the tree
  }'

# Response headers:
#   x-run-id:      this run's id
#   x-workflow-id: the workflow this run belongs to
#   access-control-expose-headers: x-run-id, x-workflow-id