Skip to content

Gateway API overview

The Axum gateway architecture, body/timeout limits, and how REST, SSE, and WebSocket transports fit together.

The Revka gateway is an Axum-based HTTP server that exposes the entire agent runtime over a structured REST API plus WebSocket and Server-Sent Events transports. The same process serves the embedded React dashboard, every /api/* route, the realtime streams, and a built-in MCP server reverse-proxied for same-origin browser access. This page is the architectural map: how the three transports fit together, the shared body and timeout limits every route inherits, the per-route overrides, and the four authentication surfaces the gateway exposes.

Read this first if you are building a client against the gateway, sizing a reverse proxy, or deciding which transport to use for a given feature. For the request-by-request reference of each route group, follow the links in Where to go next. To start the server itself, see revka gateway, daemon & service.

revka gateway starts just the HTTP/WebSocket gateway; revka daemon starts the same gateway plus channels, the heartbeat, and the cron scheduler. Both expose an identical HTTP surface, organized into five layers:

LayerPath prefixTransportAuth
Embedded dashboard/, /_app/*HTTP (static)None (SPA shell)
REST API/api/*HTTP (JSON)Bearer token
Realtime streams/api/events, /api/daemon/logsSSEBearer token
WebSocket transports/ws/*WebSocketBearer token
Public ingress/health, /metrics, /pair, /webhook, /whatsapp, /track/c/*, …HTTPNone or per-endpoint signing

The dashboard is served same-origin with the API, so the browser never crosses a CORS boundary. A separate in-process MCP server binds an ephemeral local port; the gateway reverse-proxies it under /api/mcp/* (and /ws/mcp/events) so browser clients can reach MCP tools without leaving the origin. External MCP clients read the in-process server’s address directly from ~/.revka/mcp.json.

The gateway speaks three transports. Pick the one that matches the interaction shape:

  • REST for request/response operations — config, status, CRUD on cron jobs, agents, skills, teams, workflows, and memory assets.
  • SSE for one-way broadcast streams the server pushes to all subscribers — observability events and daemon logs.
  • WebSocket for bidirectional, per-connection sessions — interactive agent chat, the PTY terminal, live canvas updates, MCP progress events, and node registration.

Every /api/* route is JSON request/response and protected by bearer-token authentication. Representative endpoint:

GET /api/status
Authorization: Bearer <token>
{
"provider": "openrouter",
"model": "anthropic/claude-sonnet-4",
"uptime_seconds": 3600,
"gateway_port": 8080,
"memory_backend": "kumiho",
"paired": true,
"channels": {},
"health": {}
}

Two SSE endpoints push server-originated streams to dashboard clients. Both require a bearer token when pairing is enabled and expect Accept: text/event-stream.

EndpointStreams
GET /api/eventsObservability broadcast: llm_request, tool_call_start, tool_call, agent_start, agent_end, error, and channel_event
GET /api/daemon/logsA live tail of the daemon stderr log — an initial ~64 KB burst, then 500 ms polling
GET /api/events
Authorization: Bearer <token>
Accept: text/event-stream

SSE is broadcast and observability-oriented; it is not a per-session conversation channel. Use /ws/chat for that.

WebSocket endpoints carry bidirectional, stateful sessions. Browsers cannot set an Authorization header on new WebSocket(), so the gateway accepts the bearer token through three fallbacks, in precedence order:

  1. Authorization: Bearer <token> header
  2. Sec-WebSocket-Protocol: bearer.<token> subprotocol
  3. ?token=<token> query parameter
EndpointPurposeSubprotocol
/ws/chatInteractive agent chat — streaming chunks, tool events, steering, stop, attachmentsrevka.v1
/ws/terminalPTY terminal that can launch a shell or an AI coding CLIrevka.v1
/ws/canvas/{id}Subscribe to live frames pushed to a named canvasrevka.v1
/ws/mcp/eventsProxy of the in-process MCP server’s session-events stream (Code tab)revka.v1
/ws/nodesExternal nodes register capabilities that become agent toolsrevka.nodes.v1
ws://host:port/ws/chat?session_id=<uuid>&name=My+Session&token=<bearer>

The PTY terminal is disabled on Android. Full frame-by-frame protocols for each WebSocket endpoint live in Realtime: WebSocket, SSE & Live Canvas.

Two limits apply to every route unless a route opts out: a request-body size cap and a request timeout.

The gateway applies a global request-body limit of 65,536 bytes (64 KiB). A request whose body exceeds this is rejected before it reaches the handler. The cap is deliberately small because the vast majority of API calls are compact JSON; routes that legitimately carry larger payloads declare their own higher limit.

A handful of endpoints raise the cap to fit their payload. These are the documented overrides:

RouteLimitWhy
PUT /api/config1 MiBA full config.toml body
POST / PUT /api/workflows, PUT /api/workflows/{*kref}16 MiBReal workflow YAML can exceed 64 KiB
POST /api/sessions/{id}/attachments25 MiBPer-file image/document upload
POST /api/skins/import25 MiBUI skin ZIP upload
POST /api/agents/avatar, POST /api/teams/avatar5 MiBAvatar image upload
PUT /api/assets/artifacts/content2 MiBArtifact content edit
GET /api/artifact-body256 MiBServing raw artifact bytes (response-side)

Every request is also bounded by a timeout, primarily to defeat slow-loris connections. The default is 30 seconds, set by REQUEST_TIMEOUT_SECS.

Terminal window
# Raise the per-request timeout to 5 minutes for long agentic calls
REVKA_GATEWAY_TIMEOUT_SECS=300 revka gateway
VariableDefaultMeaning
REVKA_GATEWAY_TIMEOUT_SECS30Overrides the global per-request timeout, in seconds.

A few endpoints carry their own longer deadlines independent of this default — for example GET /api/memory/graph allows 60 seconds for large graphs, and POST /api/nodes/{node_id}/invoke uses a 30 second hard limit per invocation.

The gateway has four distinct auth surfaces. Knowing which one a route uses is the key to integrating against it.

The primary surface. Every /api/* route, the SSE streams, and the WebSocket endpoints require a bearer token minted by the one-time pairing flow:

Authorization: Bearer <token>

A client exchanges a pairing code for a token, the gateway stores only a hash, and the token is shown exactly once. Full details — codes, the SQLite device registry, token rotation, and rate limiting — are in Pairing & authentication.

A small set of mutation endpoints are reachable only with an internal service token, not a user bearer token, because they touch privileged state. The token is auto-generated at gateway startup and persisted to <state_dir>/service-token (mode 0600 on POSIX). It is passed in a dedicated header:

X-Revka-Service-Token: <service-token>

Examples include POST /api/cost/usage (writes the budget ledger) and POST /api/auth/profiles/{id}/resolve (the only path that decrypts a stored credential). The service token is also the HMAC signing key for signed workspace asset URLs.

Administrative endpoints are bound to the loopback check: requests from a non-local IP receive 403. No bearer token is required because reachability is restricted to the host itself.

EndpointUsed by
POST /admin/shutdownrevka stop
GET /admin/paircode, POST /admin/paircode/newrevka gateway get-paircode
POST /api/channel-eventsOperator and local services publishing events

Some endpoints are intentionally unauthenticated because the caller cannot hold a bearer token — load balancers, webhook providers, or email recipients clicking a link.

  • Liveness and metrics: GET /health returns 200 with pairing status and a component snapshot and leaks no secrets; GET /metrics exposes Prometheus text format when [observability] backend = "prometheus".
  • Webhook ingress: POST /webhook, /whatsapp, /linq, /wati, /nextcloud-talk, /webhook/gmail. These verify a provider HMAC or shared secret rather than a bearer token, and are per-IP rate limited.
  • Signed URLs: GET /track/c/{encoded} (email click tracking) and GET /workspace/{*path}?exp=&sig= (HMAC-signed, time-limited workspace assets) carry their own signature instead of an Authorization header.

WebAuthn/FIDO2 hardware-key authentication is available as an optional, feature-gated surface (/api/webauthn/*, requires the webauthn compile feature). See TLS, rate limiting, WebAuthn & static serving.

A typical dashboard or client session uses all three transports at once:

  1. Pair once to obtain a bearer token, then probe GET /health (no auth) or GET /api/status (bearer) to confirm the gateway is up.

  2. Drive operations over REST — read and write config, manage cron jobs and agents, query the memory graph — each call carrying Authorization: Bearer <token> and staying under the 64 KiB cap (or the per-route override).

  3. Subscribe to /api/events over SSE for live observability, cost, and approval events.

  4. Open /ws/chat for an interactive turn, passing the token via the revka.v1 subprotocol or ?token= query parameter, and stream chunks until the done frame.