Changelog
Reference

Gateway HTTP API

The gateway's public HTTP surface — command endpoints, stream and shape proxies, deployment registration, auth, idempotency, and status codes.

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

RouteMethodPurpose
/healthGETLiveness. Public — no auth.
/api/spawnPOSTSpawn an entity.
/api/a/:type/:id/sendPOSTSend a message wake to an entity.
/api/a/:type/:id/controlPOSTApply a control verb.
/streams/*GET, HEAD, PUT, POST, DELETEProxy to the history store. Consumers read timelines and deltas with GET; the write methods carry the durable-streams append protocol.
/shapes/*GET, HEADProxy to Electric (live catalog queries).
/registry/*GET, POST, PUT, DELETE, PATCHDeployment 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 live to park the connection until new events arrive, instead of polling.
  • Cacheable. ETag and Cache-Control ride 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.

The 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

CodeWhen
200A successful read (/streams, /shapes, /health).
202A command was accepted (/api/*).
400Malformed request — a bad entity id, an unknown control verb, an invalid type.
401Missing or invalid credentials. On the read-token path, reconnect with a fresh token — the stream is resumable, so resume from your last offset.
403A read token whose prefix does not cover the requested path.
404Unknown route, missing stream path, or a command to an entity type Restate has not seen (register the deployment first).
413Request body over the cap (1 MiB default, GATEWAY_MAX_BODY_BYTES).
502The 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.