ChatGateway
A chat widget in a page cannot hold a SignalWire API token: the token carries the whole project, and
every visitor could read it. ChatGateway mounts inside a web application you already run, holds the
credentials server-side, and forwards on the widget’s behalf.
The browser learns two things, the gateway’s URL and a publishable key. Not the project, not the
Space, not the token, and not which agent runs, because the gateway injects config_url itself and a
key only ever reaches the one agent it was issued for.
It mounts on any FastAPI app. An agent already serves one, so this costs no new infrastructure:
agent.get_app().include_router(gateway.router(), prefix="/chat").
Parameters
Keyword-only.
config_url
The agent this key may talk to. Injected on every call and never accepted from the request, so whoever holds a key cannot choose which agent runs.
key
The publishable key the widget carries. Falls back to SIGNALWIRE_CHAT_GATEWAY_KEY, then to a
generated pk_ value, which is useful only for a process that also serves the page and can embed
it.
allowed_origins
Origins permitted to use this key. Localhost is always allowed so local development works unconfigured; anything else must be listed, so nothing ships open by accident.
client
The AIChatClient to forward on. Built from the environment when omitted.
secret
HMAC key for signing handles. Falls back to SIGNALWIRE_CHAT_GATEWAY_SECRET, then to a random
per-process value.
handle_ttl
Seconds a handle stays valid. Long enough to outlive a page refresh, short enough that a session left open overnight expires.
conversation_timeout
Idle seconds before the service ends a conversation, passed on every create. None leaves it to
the service default of 3600. Set here rather than in the page, because the JSON-RPC result exposes
neither the deadline nor the server’s clock, so a browser cannot discover it and two places holding
the same number drift.
max_new_conversations
New conversations allowed per window_seconds. The cap that bounds what a leaked key can cost you,
since a leaked key opens many one-turn conversations rather than hammering one.
max_turns
Turns a single conversation may run.
window_seconds
The window for max_new_conversations.
Set secret explicitly if you run more than one replica or restart often. A random per-process
secret invalidates outstanding handles on restart, and a handle signed by one replica is refused by
another.
Properties
effective_timeout
The idle timeout reported to clients. Never None, because it falls back to the service default,
so a widget is never asked to warn about a deadline it was told nothing about.
key
The publishable key this gateway accepts, whether you passed one or it generated one.
Exceptions
GatewayRejection
A request the gateway refused, carrying .status and .reason. Reasons are deliberately coarse,
because anything finer would let a caller map the caps and the allowlist by probing.
The browser-facing wire
One endpoint: POST {prefix}/ with the key in Authorization: Bearer. With prefix="/chat" the
path is /chat/, since the router mounts POST /.
A chat with no handle mints a conversation, so start is how you get a greeting before the visitor
types rather than a required first step. Anything outside these four methods is a 400.
If method is omitted, it defaults to chat. start can also reuse a valid existing handle.
Rejections carry an HTTP status and {"error": "<reason>"}: 400 malformed, 401 bad key, 403
bad or expired handle or a disallowed origin, 429 a cap was hit.
What a leaked key can do
A publishable key is public by definition, so design for it being taken.
A key alone cannot read a conversation. Without a handle there is no conversation to attach
log to, and handles are HMAC-signed so they cannot be forged, guessed, or enumerated. A caller
holding a valid handle can use log to read that conversation’s filtered user and assistant
transcript, but not its system prompt or tool traffic.
It can start conversations and send messages. Use max_new_conversations and max_turns to
bound that usage. The first limits newly created conversations per rolling window; the second limits
turns in each conversation.
Counters live in the serving process. Behind several replicas each keeps its own, so the effective cap multiplies by replica count. Set them with that in mind, or put a shared limiter in front.
The origin allowlist stops a key pasted into someone else’s page, because a browser sends their
origin and the gateway refuses it. It does not stop curl, which simply omits the header. Treat it
as leak containment, not access control. The same list drives CORS, including
Access-Control-Expose-Headers: X-Chat-Handle and a preflight response.
Rotating a key
There is no expiry on the key itself, because publishable keys live in static pages and a TTL means
the widget dies silently at some point. Rotate on demand instead: construct the gateway with a new
key and redeploy. Change secret at the same time to invalidate outstanding handles too.
Methods
The APIRouter to mount on your app.
Issue a signed handle server-side.
Verify a handle and return its conversation id.
Validate a browser request without FastAPI.
The transcript a browser may redraw.
Epoch seconds of the newest message.
Close the client, when the gateway built it.