Gateway HTTP API
The gateway is the single front door: your app, your UI, and the CLI reach teaspill only through it. The frontend SDK's actions client wraps this API, but the HTTP surface is public and stable — you can call it directly.
Base URL and auth
The gateway listens on port 8787 by default (http://localhost:8787 in local dev). Every route except GET /health requires an API key:
Authorization: Bearer <api-key>
See Auth & API keys for minting keys and the optional browser read-token path.
Routes
| Route | Method | Purpose |
|---|---|---|
/health | GET | Liveness. Public — no auth. |
/api/spawn | POST | Spawn an entity. |
/api/a/:type/:id/send | POST | Send a message wake to an entity. |
/api/a/:type/:id/control | POST | Apply a control verb. |
/streams/* | GET, HEAD, PUT, POST, DELETE | Proxy to the history store. Consumers read timelines and deltas with GET; the write methods carry the durable-streams append protocol. |
/shapes/* | GET, HEAD | Proxy to Electric (live catalog queries). |
/registry/* | GET, POST, PUT, DELETE, PATCH | Deployment registration. |
Commands (/api/*) also accept the fully-qualified form with an explicit tenant, for example POST /api/t/:tenant/a/:type/:id/send, accepted only for this deployment's own tenant.
Commands
All three command endpoints are one-way durable sends. The response is 202 Accepted with the entity URL and its timeline stream, so a caller immediately knows where to watch:
{
"url": "/t/default/a/researcher/01j9z8k3q…",
"streamPath": "/t/default/agents/researcher/01j9z8k3q…/timeline",
"streamUrl": "/streams/t/default/agents/researcher/01j9z8k3q…/timeline",
"restate": { "invocationId": "inv_…" }
}
Spawn
POST /api/spawn with a JSON body:
{ type: string, id?: string, args?: Json, parent?: string, workspaceRef?: string }
id defaults to a fresh lowercase ULID; supply one for deterministic spawn. args is validated by the agent's spawn schema. parent is the parent entity URL. workspaceRef is an optional workspace key (<tenant>/<name>) placing the new agent in a chosen — typically shared — workspace; omit it and the agent gets its own private workspace.
curl -X POST http://localhost:8787/api/spawn \
-H "Authorization: Bearer $TEASPILL_API_KEY" \
-H "Content-Type: application/json" \
-d '{"type":"researcher","args":{"topic":"the history of tea"}}'
Send
POST /api/a/:type/:id/send. The body is the message, forwarded verbatim as a wake — the gateway never rewrites it, so use the canonical message shape unless your deployment normalizes shorthand (Building agents covers both).
curl -X POST http://localhost:8787/api/a/researcher/r1/send \
-H "Authorization: Bearer $TEASPILL_API_KEY" \
-H "Content-Type: application/json" \
-d '{"kind":"message","content":[{"type":"text","text":"add a section on green tea"}]}'
Control
POST /api/a/:type/:id/control with a verb:
{ verb: "interrupt" | "pause" | "resume" | "archive", reason?: string }
curl -X POST http://localhost:8787/api/a/researcher/r1/control \
-H "Authorization: Bearer $TEASPILL_API_KEY" \
-H "Content-Type: application/json" \
-d '{"verb":"interrupt","reason":"user cancelled"}'
Idempotency
Send an Idempotency-Key header on any command and the gateway forwards it verbatim, so a retried request is deduplicated within the retention window (default 24 h) instead of spawning or sending twice.
curl -X POST http://localhost:8787/api/spawn \
-H "Authorization: Bearer $TEASPILL_API_KEY" \
-H "Idempotency-Key: spawn-researcher-nightly-2026-07-19" \
-H "Content-Type: application/json" \
-d '{"type":"researcher","id":"nightly","args":{"topic":"tea"}}'
Reading streams
GET /streams/<path> proxies the history store byte-for-byte. Use it to read an entity's timeline or deltas; the stream URL comes back in every command's 202 response, or derive it with the addressing helpers. The route also forwards the durable-streams write protocol (PUT to create a stream, POST to append) so the projection path can publish through the same front door — as a consumer you only ever issue GET.
The read protocol passes through untouched:
- Resumable. Every response carries a next-offset header; reconnect from your last offset to continue exactly where you left off.
- Long-poll. Request
liveto park the connection until new events arrive, instead of polling. - Cacheable.
ETagandCache-Controlride catch-up reads, so already-read history is cheap.
Reads are ordered by seq and gapless (see Event schema); a gap means drift, and the reducer in the frontend SDK surfaces it. Consuming and rendering a stream is covered in Frontend integration.
Shapes
GET /shapes/v1/shape?… proxies to Electric, which serves the catalog as live-updating queries. Filter by scalar columns — type, status, parent, tenant. This is how lists and dashboards stay live; the frontend SDK's catalog client wraps it.
Registry
/registry/* forwards deployment registration to Restate's admin API. Registration is how your agent-loop and executor services become reachable; the CLI and the reference deployment drive it for you, and Self-hosting covers the networking rules.
uri you register is dialed directly by Restate from inside its container — that traffic does not pass through the gateway, and the gateway performs no URL rewriting. Register a compose-network service by its service name (http://agent-loop:9080) and a host-run service as http://host.docker.internal:<port> — never localhost, which resolves to the container itself and fails on the first invocation.Status codes
| Code | When |
|---|---|
200 | A successful read (/streams, /shapes, /health). |
202 | A command was accepted (/api/*). |
400 | Malformed request — a bad entity id, an unknown control verb, an invalid type. |
401 | Missing or invalid credentials. On the read-token path, reconnect with a fresh token — the stream is resumable, so resume from your last offset. |
403 | A read token whose prefix does not cover the requested path. |
404 | Unknown route, missing stream path, or a command to an entity type Restate has not seen (register the deployment first). |
413 | Request body over the cap (1 MiB default, GATEWAY_MAX_BODY_BYTES). |
502 | The gateway could not reach an upstream service. |
CORS
Browsers read /streams/* and /shapes/* cross-origin, so the gateway answers the preflight and sets response CORS headers for GET on those two route families only — never /api/* or /registry/*. Offset, cursor, and etag headers are exposed so a browser can read them, and CORS headers ride 401/403 rejections too, so a browser can react. Set GATEWAY_CORS_ALLOW_ORIGINS to pin specific origins; the default * is safe because a read token still gates the actual read.