edge routes / API
every HTTP route on the edge Worker โ method, path, shape, auth.
edge routes
one Cloudflare Worker hosts all of these plus the SessionDO. CORS allow-origin is
ALLOWED_ORIGIN (default *); OPTIONS is answered for every route.
public routes
| method | path | body / query โ response |
|---|---|---|
POST | /api/chat | { sessionId, message, tenantId?, history? } โ { reply, handoff, handedOff, degraded?, formId?, form? } |
POST | /api/contact | { sessionId, tenantId?, name?, contact?, message? } โ { ok: true } โ legacy contact-capture shim (maps onto the lead fan-out) |
POST | /api/lead | { tenantId?, sessionId, formId?, values, history } โ { ok: true } โ a form's captured values fan out to delivery connectors |
GET | /api/session/:id/ws?t=<tenant> | WebSocket upgrade โ SessionDO. 426 if not an upgrade. the visitor's live channel |
GET | /api/widget/config?t=<tenant> | โ { theme: {...} } โ the public, secret-free widget projection (whitelist only) |
GET | /api/usage?t=<tenant> | โ { tenantId, usage: { ai, handoff, tokens }, plan, entitled, status, limits, withinLimits } |
GET | /health | โ { status: "ok", service: "edge" } |
POST /api/chat gate
before answering, the Worker checks entitlement + plan limits. self-host ("self") is
always entitled and unmetered. a non-self tenant with no valid snapshot gets
402 { error: "subscription_required" }; over its cap it gets
429 { error: "usage_limit_reached" }. missing sessionId/message โ 400.
secret-guarded routes
these carry a bot token or mutate config, so they require a shared-secret header. a missing or wrong secret returns 401 (config routes) / 403 (webhook, billing) and leaks nothing.
| method | path | auth header | purpose |
|---|---|---|---|
POST | /api/telegram/webhook | x-telegram-bot-api-secret-token: <TELEGRAM_WEBHOOK_SECRET> | owner's Telegram reply โ push to the visitor via the DO, set handedOff |
GET | /api/tenant/config?t=<tenant> | x-tenant-sync-secret: <TENANT_SYNC_SECRET> | read a tenant's full config { botToken, chatId, systemPrompt?, model?, ... } (404 if none) |
POST | /api/tenant/config | x-tenant-sync-secret: <TENANT_SYNC_SECRET> | { tenantId, config } โ merge into the tenant's KV config. the CLI writes here |
POST | /api/billing/entitlement | x-billing-sync-secret: <BILLING_SYNC_SECRET> | mirror an entitlement snapshot into KV (for a hosted/multi-tenant deploy; unused in self-host) |
GET /api/tenant/config returns the full config (bot token included) and is secret-guarded.
GET /api/widget/config is the public read the browser uses โ it returns only the theme
whitelist and never the token/prompt. don't confuse them. see security.
the SessionDO (internal)
the Worker forwards to a SessionDO per (tenant, session); the browser never calls it
directly. its surface: the WebSocket upgrade (visitor connects, receives
{ type: "ready", handedOff }), GET /state ({ handedOff }), POST /operator {text}
(set handedOff, broadcast the operator reply), and POST /handoff (broadcast a handoff
prompt). server โ browser events: ready, operator, handoff, resume.
keeping this in sync
the repo mirrors routes into a Bruno collection (api-collection/). when you add or change
a route, update the matching .bru request in the same change (per CONTRIBUTING.md).