> Fetch clean Markdown by appending `.md` to any page URL under https://signalwire.com/docs or requesting it with the HTTP header `Accept: text/markdown`. The root index at https://signalwire.com/docs/llms.txt lists the available documentation indexes. # ChatGateway > Browser-facing proxy that lets a page chat with an AI agent without holding a SignalWire API token. [aichatclient]: /docs/server-sdks/reference/python/agents/ai-chat-client [router]: /docs/server-sdks/reference/python/agents/chat-gateway/router [minthandle]: /docs/server-sdks/reference/python/agents/chat-gateway/mint-handle [readhandle]: /docs/server-sdks/reference/python/agents/chat-gateway/read-handle [prepare]: /docs/server-sdks/reference/python/agents/chat-gateway/prepare [visiblemessages]: /docs/server-sdks/reference/python/agents/chat-gateway/visible-messages [lastactivity]: /docs/server-sdks/reference/python/agents/chat-gateway/last-activity [close]: /docs/server-sdks/reference/python/agents/chat-gateway/close [handoffrouter]: /docs/server-sdks/reference/python/agents/handoff-router [mount]: /docs/server-sdks/reference/python/agents/agent-base/mount 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. ```python from signalwire.ai_chat import ChatGateway gateway = ChatGateway( config_url="https://bayview-taxi.example.com/swml", key="pk_your_publishable_key", allowed_origins=["https://bayviewtaxi.example.com"], ) app.include_router(gateway.router(), prefix="/chat") ``` It mounts on any FastAPI app. An agent already serves one, so this costs no new infrastructure: `agent.mount(gateway.router(), prefix="/chat")` (see [`mount()`][mount]). > **Note** > > The SignalWire address widget also calls `/handoff`, `/escalate`, and `/say` at the same URL to > move a conversation between chat and a phone call. [`HandoffRouter`][handoffrouter] serves those > three routes; mount it at the same prefix as the gateway. ## **Parameters** Keyword-only. **`config_url`** `str` — required 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`** `str | None` — default: None 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`** `list[str] | tuple[str, ...]` — default: () 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`** `AIChatClient | None` — default: None The [`AIChatClient`][aichatclient] to forward on. Built from the environment when omitted. --- **`secret`** `bytes | str | None` — default: None HMAC key for signing handles. Falls back to `SIGNALWIRE_CHAT_GATEWAY_SECRET`, then to a random per-process value. --- **`handle_ttl`** `int` — default: 86400 Seconds a handle stays valid. Long enough to outlive a page refresh, short enough that a session left open overnight expires. --- **`conversation_timeout`** `int | None` — default: None 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`** `int` — default: 60 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`** `int` — default: 200 Turns a single conversation may run. --- **`window_seconds`** `int` — default: 60 The window for `max_new_conversations`. --- > **Warning** > > 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`** `int` 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`** `str` The publishable key this gateway accepts, whether you passed one or it generated one. --- ## **Exceptions** **`GatewayRejection`** `Exception` 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 /`. | Method | Body | Returns | | ------- | ----------------------------------------- | ------------------------------------------------------------------------------------ | | `start` | `{"method": "start", "handle"?}` | `{greeting, status, timeout}` plus an `X-Chat-Handle` header when a handle is minted | | `chat` | `{"method": "chat", "handle", "message"}` | the service's JSON-RPC envelope | | `log` | `{"method": "log", "handle"}` | `{messages, timeout, last_activity}` | | `end` | `{"method": "end", "handle"}` | `{"status": "ended"}` | 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": ""}`: `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** #### [router](/docs/server-sdks/reference/python/agents/chat-gateway/router) The APIRouter to mount on your app. #### [mint\_handle](/docs/server-sdks/reference/python/agents/chat-gateway/mint-handle) Issue a signed handle server-side. #### [read\_handle](/docs/server-sdks/reference/python/agents/chat-gateway/read-handle) Verify a handle and return its conversation id. #### [prepare](/docs/server-sdks/reference/python/agents/chat-gateway/prepare) Validate a browser request without FastAPI. #### [visible\_messages](/docs/server-sdks/reference/python/agents/chat-gateway/visible-messages) The transcript a browser may redraw. #### [last\_activity](/docs/server-sdks/reference/python/agents/chat-gateway/last-activity) Epoch seconds of the newest message. #### [close](/docs/server-sdks/reference/python/agents/chat-gateway/close) Close the client, when the gateway built it. ## **Example** ```python {10,12,13} from fastapi import FastAPI from signalwire.ai_chat import ChatGateway app = FastAPI() gateway = ChatGateway( config_url="https://bayview-taxi.example.com/swml", # never leaves the server key="pk_your_publishable_key", # safe in the page allowed_origins=["https://bayviewtaxi.example.com"], secret="a-stable-secret-across-replicas", conversation_timeout=1800, max_new_conversations=60, max_turns=200, ) app.include_router(gateway.router(), prefix="/chat") ``` > Browser-facing proxy that lets a page chat with an AI agent without holding a SignalWire API token. ## Docs - [close](https://signalwire.com/docs/server-sdks/reference/python/agents/chat-gateway/close.md): Close the AIChatClient the gateway created. - [last_activity](https://signalwire.com/docs/server-sdks/reference/python/agents/chat-gateway/last-activity.md): Find when a conversation was last touched, so a reload can resume its idle clock. - [mint_handle](https://signalwire.com/docs/server-sdks/reference/python/agents/chat-gateway/mint-handle.md): Issue a signed handle for a conversation, server-side. - [prepare](https://signalwire.com/docs/server-sdks/reference/python/agents/chat-gateway/prepare.md): Validate a browser request and build the upstream JSON-RPC call, without FastAPI. - [read_handle](https://signalwire.com/docs/server-sdks/reference/python/agents/chat-gateway/read-handle.md): Verify a signed handle and return the conversation id inside it. - [router](https://signalwire.com/docs/server-sdks/reference/python/agents/chat-gateway/router.md): Build the APIRouter that exposes the gateway to a browser. - [visible_messages](https://signalwire.com/docs/server-sdks/reference/python/agents/chat-gateway/visible-messages.md): Filter a conversation down to the dialogue a browser may redraw.