HandoffRouter
ChatGateway lets a browser hold a text conversation. HandoffRouter is the other
half the SignalWire address widget expects: three routes, served at the same URL prefix as the
gateway, that move that conversation to a phone call and back, and let a visitor type into a live
call.
Every route checks the request origin against the gateway’s allowlist. Unknown, expired, and
already-redeemed nonces all answer 404, so a caller can’t probe whether a given call is live.
How the nonce works
A browser can’t be trusted to name a call, since a page-supplied call ID would let anyone inject
speech into a stranger’s call. Instead the browser proves which call it is on. Your application puts
a random handoff_nonce in the user variables of one dial, registers it against that
call’s IDs from the dynamic-config callback, and the browser presents it later. The same dial
carries the chat_handle, so the callback recovers the conversation ID with
read_handle().
Redemption for a handle is single use. Typing is repeatable for the life of the call, bounded by
max_messages_per_call.
A new medium never starts until the one it replaces has finished and your capture_leg callback has
confirmed its record is durable. Without that wait the new leg’s config fetch races a record that is
still being written and opens knowing nothing.
Like the gateway’s rate-limit counters, the registry lives in the serving process. A redemption must
reach the replica that served the dial. Run one replica, use sticky routing, or pass a shared
registry.
Parameters
Keyword-only.
gateway
The gateway that owns the conversations. Used to issue handles and to check origins, so both halves of the URL enforce the same origin policy.
capture_leg
Called as capture_leg(conversation_id, medium) to end a leg and write its record. Return a truthy
value only once that record is durable. Sync or async. When omitted, no wait happens and the
ordering guarantee isn’t provided.
end_call
Called as end_call(call_id) to hang the call up server-side so its teardown hooks fire
immediately.
send_message
Called as send_message(call_id, text) for /say. Omit to leave typing disabled; the route then
answers 404.
next_conversation_id
Called as next_conversation_id(conversation_id) to produce the ID for the new leg. An ended
conversation can’t be reopened, so a fresh ID is required. Defaults to appending .1, or
incrementing an existing .N suffix.
nonce_ttl
Seconds a nonce stays redeemable.
max_messages_per_call
Ceiling on typed messages for one call. Each is a billable turn, so this is a spend guard as much as an abuse guard.
capture_timeout
Seconds to wait for capture_leg. A ceiling, not a budget; capture is normally sub-second. On
timeout the next medium starts without this leg’s record and a warning is logged.
registry
Shared mapping for the nonce table. Supply one backed by shared storage to run more than one replica.
Properties
NonceEntry
What a nonce is a capability for. Importable from signalwire.ai_chat.
NonceEntry.conversation_id
The conversation the nonce belongs to.
NonceEntry.call_id
The call it was registered against.
NonceEntry.issued_at
Monotonic timestamp used for expiry.
NonceEntry.messages
Typed messages delivered so far.
Methods
Record what a nonce is a capability for.
Exchange a nonce for a chat handle. Single use.
End a chat leg and wait for its record.
Deliver typed text into a live call.
The APIRouter to mount beside the gateway’s.
Example
Register the nonce from the dynamic-config callback of the dial that carried it, reading the call ID
from the platform’s request rather than from anything the browser sent. The
capabilities helpers read the same user variables.