> 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/typescript/agents/ai-chat-client [rawpost]: /docs/server-sdks/reference/typescript/agents/ai-chat-client/raw-post [router]: /docs/server-sdks/reference/typescript/agents/chat-gateway/router [minthandle]: /docs/server-sdks/reference/typescript/agents/chat-gateway/mint-handle [readhandle]: /docs/server-sdks/reference/typescript/agents/chat-gateway/read-handle [prepare]: /docs/server-sdks/reference/typescript/agents/chat-gateway/prepare [visiblemessages]: /docs/server-sdks/reference/typescript/agents/chat-gateway/visible-messages [lastactivity]: /docs/server-sdks/reference/typescript/agents/chat-gateway/last-activity [close]: /docs/server-sdks/reference/typescript/agents/chat-gateway/close [handoffrouter]: /docs/server-sdks/reference/typescript/agents/handoff-router A chat widget in a page can't 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 on the server, and forwards on the widget's behalf. The browser learns two things, the gateway's URL and a publishable key. It doesn't learn the project, the Space, the token, or which agent runs, because the gateway sends `config_url` itself and a key only reaches the one agent it was issued for. ```typescript import { AgentBase, ChatGateway } from '@signalwire/sdk'; const agent = new AgentBase({ name: 'dispatch', route: '/dispatch' }); agent.setPromptText('You are Ada, the dispatcher for Bayview Taxi.'); const gateway = new ChatGateway({ configUrl: 'https://bayview-taxi.example.com/dispatch', // never leaves the server key: '', // safe in the page allowedOrigins: ['https://bayviewtaxi.example.com'], secret: process.env.SIGNALWIRE_CHAT_GATEWAY_SECRET, conversationTimeout: 1800, maxNewConversations: 60, maxTurns: 200, }); agent.mount(gateway.router(), { prefix: '/chat' }); await agent.serve(); ``` ```bash npm install @signalwire/sdk ``` An agent already serves a Hono app, so `agent.mount()` hosts the gateway with no new infrastructure. [`router()`][router] returns a standard Hono app, so `app.route()` works on any other Hono server. > **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 The constructor takes one options object, `ChatGatewayOptions`. It throws an `Error` when `configUrl` is empty, and, when `client` is omitted, when `SIGNALWIRE_PROJECT_ID` isn't set or no service URL resolves from `SIGNALWIRE_SPACE`. **`configUrl`** `string` — required The agent this key may talk to. Sent on every call and never taken from the request, so whoever holds a key can't choose which agent runs. --- **`key`** `string` The publishable key the widget carries. Falls back to `SIGNALWIRE_CHAT_GATEWAY_KEY`, then to a generated `pk_` value, which is useful only to a process that also serves the page and can embed it. --- **`allowedOrigins`** `readonly string[]` Origins allowed to use this key, stored without trailing slashes. Localhost is always allowed so local development works unconfigured; any other origin must be listed. --- **`client`** `AIChatClient` The [`AIChatClient`][aichatclient] to forward with. Built from the environment when omitted (`SIGNALWIRE_PROJECT_ID`, `SIGNALWIRE_API_TOKEN`, and `SIGNALWIRE_SPACE`, which the examples on these pages assume are set), and then closed by [`close()`][close]. A client you pass in is yours to close. --- **`secret`** `string | Uint8Array` HMAC key for signing handles. Falls back to `SIGNALWIRE_CHAT_GATEWAY_SECRET`, then to a random per-process value. --- **`handleTtl`** `number` — default: 86400 Seconds a handle stays valid: long enough to outlive a page refresh, short enough that a session left open overnight expires. --- **`conversationTimeout`** `number | null` Idle seconds before the service ends a conversation, sent on every create. Omit it for the service default of 3600. Set it here rather than in the page: the service's responses don't expose the deadline, so the gateway reports it to the widget on `start` and `log`. --- **`maxNewConversations`** `number` — default: 60 New conversations allowed per `windowSeconds`. The cap that matters most: a leaked key is used to open many one-turn conversations, each billed for its opening turn. --- **`maxTurns`** `number` — default: 200 Turns one conversation may run, in total. --- **`windowSeconds`** `number` — default: 60 The window `maxNewConversations` counts over, in seconds. --- > **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 **`configUrl`** `string` The agent config sent on every call. Read-only. --- **`key`** `string` The publishable key the browser presents, whether you passed one or the gateway generated it. Read-only. --- **`allowedOrigins`** `ReadonlySet` The listed origins, without trailing slashes. Read-only. --- **`handleTtl`** `number` Seconds a handle stays valid. --- **`conversationTimeout`** `number | null` Idle seconds before the service ends a conversation, or `null` for the service default. --- **`maxNewConversations`** `number` New conversations allowed per window. --- **`maxTurns`** `number` Turns allowed per conversation. --- **`windowSeconds`** `number` Window for `maxNewConversations`, in seconds. --- **`effectiveTimeout`** `number` The idle timeout reported to the widget. Never `null`: it falls back to the service default of 3600, so the widget is never asked to warn about a deadline it wasn't told. Read-only. --- ## Exports All of these are importable from `@signalwire/sdk`. **`GatewayRejection`** `class extends Error` A request the gateway refused. Carries `status`, the HTTP status to return (400, 401, 403, 413 or 429\), and `reason`, a short explanation that is safe to show the browser. Reasons name a category and nothing finer, because anything finer would let a caller map the caps and the allowlist by probing. --- **`ChatGatewayOptions`** `interface` The constructor's options object, described under Parameters. --- **`MAX_REQUEST_BODY_BYTES`** `number` `65536` (64 KiB). The largest request body the router accepts. A larger body is answered with `413` before it is parsed. --- **`MAX_MESSAGE_BYTES`** `number` `8192` (8 KiB of UTF-8). The largest chat message, refused with `413` before a conversation is created or a turn is counted. [`HandoffRouter`][handoffrouter] applies the same limit to the text of `/say`. --- **`MAX_USER_METADATA_BYTES`** `number` `8192` (8 KiB). The largest `user_meta_data` object, measured serialized, refused with `413`. --- ## The browser-facing wire One endpoint: `POST /` on the router, with the key in `Authorization: Bearer`. Mounted at `/chat`, the path is `/chat/`. A preflight `OPTIONS /` is answered for allowed origins. The body is JSON with a `method` of `start`, `chat`, `log`, or `end`; `method` defaults to `chat`. | Method | Body fields | Returns | | ------- | --------------------------------------- | ------------------------------------------------- | | `start` | `handle?`, `user_meta_data?` | `{ greeting, status, timeout }` | | `chat` | `handle?`, `message`, `user_meta_data?` | The service's JSON-RPC response, streamed through | | `log` | `handle` | `{ messages, timeout, last_activity }` | | `end` | `handle` | `{ status: 'ended' }` | A `start` or `chat` with no handle creates a conversation and returns its handle in the `X-Chat-Handle` response header. `start` opens the conversation with no user message, so the agent speaks first. `log` and `end` require a handle. Any other `method` is a `400`. Rejections return `{ error: reason }` with the status: `400` malformed request or handle, `401` bad key, `403` origin not allowed or a bad or expired handle, `413` a request, message, or metadata over its limit, `429` a cap was reached. A failure reaching the chat service before any reply is sent returns `502` with `{ error: 'chat service error' }`. ### Page context `user_meta_data` is the one field forwarded rather than replaced: the page context a widget collects about itself, such as URL, title, referrer, locale, and viewport. It reaches the agent's config request as `params.user_meta_data`, so a chat agent can tailor its greeting. The service reads it when it creates a conversation, which is the `start` call or whichever `chat` auto-creates one. Metadata sent on later turns is accepted and unused. The browser writes it, so treat it as the visitor's claim about themselves, never as authority; a page can put anything in it, including text aimed at your prompt. ## What a leaked key can do A publishable key is public by definition, so design for it being taken. A key alone can't read a conversation. The full `chat_log` isn't exposed, and handles are signed by the gateway, so they can't be forged, guessed, or enumerated. `log` returns only the user and assistant turns. It can start and continue conversations, and every turn is billed. `maxNewConversations` and `maxTurns` bound that: the first limits conversations created per window, the second limits turns in each. The caps are counted in the serving process. Behind several replicas each keeps its own, so the effective cap multiplies by the 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 that page's origin and the gateway refuses it. It doesn't stop a script that sends no origin or a false one. Treat it as leak containment, not access control. To rotate a key, 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/typescript/agents/chat-gateway/router) The Hono app to mount on your server. #### [mintHandle](/docs/server-sdks/reference/typescript/agents/chat-gateway/mint-handle) Issue a signed handle server-side. #### [readHandle](/docs/server-sdks/reference/typescript/agents/chat-gateway/read-handle) Verify a handle and return its conversation id. #### [prepare](/docs/server-sdks/reference/typescript/agents/chat-gateway/prepare) Validate a browser request without the router. #### [visibleMessages](/docs/server-sdks/reference/typescript/agents/chat-gateway/visible-messages) The transcript a browser may redraw. #### [lastActivity](/docs/server-sdks/reference/typescript/agents/chat-gateway/last-activity) Epoch seconds of the newest message. #### [close](/docs/server-sdks/reference/typescript/agents/chat-gateway/close) Close the client, when the gateway built it. ### checkKey Verifies the publishable key a request presented, comparing in constant time. [`router()`][router] and [`prepare()`][prepare] call it for you. **`presented`** `string | null | undefined` — required The key from the request, or `null` or `undefined` when absent. --- Returns `void`. Throws `GatewayRejection` with status `401` and reason `bad key` when the key is missing or wrong. ### checkOrigin Allows localhost and listed origins, and refuses any other. A missing `Origin` is allowed: browsers send one on the cross-origin requests this serves, so its absence means a caller that isn't a browser, and refusing it would break server-side use without stopping an attacker. **`origin`** `string | null | undefined` — required The request's `Origin` header, or `null` or `undefined` when absent. --- Returns `void`. Throws `GatewayRejection` with status `403` and reason `origin not allowed` for an origin that isn't allowed. ### readUserMetadata Validates the `user_meta_data` field of a browser request. Absent, `null`, and empty all yield `null`. **`body`** `Record` — required The browser's request body. --- Returns `Record | null`. Throws `GatewayRejection` with status `400` when the field isn't a JSON object or can't be serialized, and `413` when it is larger than `MAX_USER_METADATA_BYTES` serialized. ## Example Checking a request's key and origin by hand, and reading its page context, in a handler you wrote yourself: ```typescript import { ChatGateway, GatewayRejection } from '@signalwire/sdk'; const gateway = new ChatGateway({ configUrl: 'https://bayview-taxi.example.com/dispatch', key: '', allowedOrigins: ['https://bayviewtaxi.example.com'], }); export function guard(origin: string | undefined, authorization: string | undefined): void { const key = authorization?.startsWith('Bearer ') ? authorization.slice(7) : null; try { gateway.checkKey(key); gateway.checkOrigin(origin); } catch (err) { if (err instanceof GatewayRejection) { console.warn(err.status, err.reason); // 401 bad key, 403 origin not allowed } throw err; } } export function pageContext(body: Record) { return gateway.readUserMetadata(body); // Record | null } ``` ## Docs - [close](https://signalwire.com/docs/server-sdks/reference/typescript/agents/chat-gateway/close.md): Close the AI chat client, when the gateway built it. - [lastActivity](https://signalwire.com/docs/server-sdks/reference/typescript/agents/chat-gateway/last-activity.md): Get the epoch time of the newest message in a conversation. - [mintHandle](https://signalwire.com/docs/server-sdks/reference/typescript/agents/chat-gateway/mint-handle.md): Issue a signed handle that names a conversation. - [prepare](https://signalwire.com/docs/server-sdks/reference/typescript/agents/chat-gateway/prepare.md): Validate a browser request and build the call to the chat service. - [readHandle](https://signalwire.com/docs/server-sdks/reference/typescript/agents/chat-gateway/read-handle.md): Verify a handle and return the conversation id inside it. - [router](https://signalwire.com/docs/server-sdks/reference/typescript/agents/chat-gateway/router.md): Build the Hono app that exposes the gateway to a browser. - [visibleMessages](https://signalwire.com/docs/server-sdks/reference/typescript/agents/chat-gateway/visible-messages.md): Reduce a conversation log to the turns a browser may redraw.