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
| Method | Path | Purpose |
|---|---|---|
| GET | /health | public liveness |
| POST | /v1/signup | create a tenant + mint the first API key (no auth) |
| POST | /v1/runs | start a run; returns an SSE event stream |
| GET | /v1/runs | list runs (filters: status, roots, workflow) |
| GET | /v1/runs/:id | run metadata |
| GET | /v1/runs/:id/events | full event log (ordered) |
| POST | /v1/runs/:id/tool-results/:toolUseId | SDK posts a custom tool output |
| GET | /v1/workflows/:id | full run tree + aggregated cost for a workflow |
| POST | /v1/keys | mint a new API key (for rotation) |
| GET | /v1/keys | list this tenant's keys (no secrets) |
| DELETE | /v1/keys/:id | revoke a key |
| GET | /v1/audit | security-relevant audit log for this tenant |
| PUT | /v1/credentials/:provider | upload or rotate provider key |
| GET | /v1/credentials | list (no secrets returned) |
| DELETE | /v1/credentials/:provider | revoke |
| GET | /v1/memories?namespace=&limit= | list memories |
| DELETE | /v1/memories/:id | delete one |
| DELETE | /v1/memories?namespace= | clear a namespace |
| POST | /v1/transcribe | audio → text (Whisper, multipart upload) |
| POST | /v1/synthesize | text → audio stream (OpenAI TTS) |
| GET | /internal/runs/:id/tool-result/:toolUseId | runtime 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
| Status | Cause |
|---|---|
| 400 | missing model/input; unknown model family; no credential for that provider |
| 401 | missing or invalid bearer token |
| 502 | runtime 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/openaiMemories
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.mp3Full 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