docs

Docs › Build

Realtime events

Every event your webhooks receive can also arrive over a WebSocket, the moment it commits — the low-latency channel UIs want. Webhooks stay the durable record; the stream is the refresh signal.

1. Mint a token from your backend

Your API key never reaches the browser. Your server mints a short-lived token (one hour) and hands it to the client:

cURL
# → { "token": "hsrt_…", "url": "wss://media.handset.dev/v1/events", "expires_at": … }
curl -X POST https://api.handset.dev/v1/realtime/tokens \
  -H "Authorization: Bearer $HANDSET_API_KEY"
TypeScript
import { mintRealtimeToken } from "@handset/sdk";

const grant = await mintRealtimeToken();
// grant.data → { token, url, expires_at }
Python
from handset.api.events import mint_realtime_token

grant = mint_realtime_token.sync(client=client)
# grant.token, grant.url, grant.expires_at

Keys scoped to a tenant mint tenant-scoped tokens — the stream only ever carries that tenant's events.

2. Connect and listen

Each frame is the same envelope your webhook endpoints receive — one event catalog everywhere (see Webhooks):

browser.js
const ws = new WebSocket(`${grant.url}?token=${grant.token}`);
ws.onmessage = (msg) => {
  const event = JSON.parse(msg.data);
  // { id, type: "message.received", tenant_id, created_at, data }
  if (event.type === "call.transcript") render(event.data);
};

Tokens expire after an hour and the socket closes — reconnect with a fresh mint. Delivery is best-effort by design: treat events as a signal to refetch, keep a slow poll as the safety net, and let webhooks remain the durable channel.

Zero-code option: Handset UI

If you use Handset UI, one prop does all of the above — token minting through your proxy, connection management, and instant hook refreshes:

app.tsx
<HandsetProvider baseUrl="/api/handset" realtime>
  <App />
</HandsetProvider>

What arrives

Everything in the event catalog: message.received, message.delivered, call.started, call.transcript (per utterance, mid-call), call.summary, call.stream.*, voicemail.created, and the rest. Account-scoped by your key, tenant-filtered when the token is tenant-scoped.