Realtime WebSocket
Stream every TipPage event live over a plain WebSocket - no SDK, any language. Connect with your API key, or hand short-lived tokens to clients that shouldn't see it.
The webhook events, live over a WebSocket at wss://ws.tippage.com. Same envelope, same event id. Webhooks are retried for ~44 hours; the stream only delivers what happens while you’re connected. Use the stream to react now, webhooks when nothing may be lost - or both, deduped on id.
Connecting
Any WebSocket client works. Send one JSON line, then just receive:
const ws = new WebSocket("wss://ws.tippage.com");
ws.onopen = () => ws.send(JSON.stringify({ data: { apiKey: process.env.TIPPAGE_API_KEY } }));
ws.onmessage = (e) => {
const frame = JSON.parse(e.data);
if (!frame.pub) return; // connect frame or {} keepalive
const event = frame.pub.data; // the webhook envelope
console.log(event.type, event.data);
};
{ "connect": { "client": "...", "subs": { "dev:t_abc123:tts": {} }, "ping": 25 } } // first frame
{ "channel": "dev:t_abc123:tts", "pub": { "data": { "id": "evt_...", "type": "tip.created", ... } } }
{} // keepalive, ~every 25s
Reconnect with backoff when it closes. Close codes 4500-4999 mean fix the credential first:
| Code | Meaning |
|---|---|
4501 |
Bad key, or an expired/malformed token |
4507 |
No realtime-capable scopes |
4508 |
Key rotated, disabled, deleted or re-scoped (checked about every 30s) |
Handoff tokens
Your key never goes to a browser, an app you ship or anyone else’s machine. For those, your server mints a short-lived token:
curl -X POST https://api.tippage.com/v1/realtime/token \
-H "Authorization: Bearer tp_live_..." -H "Content-Type: application/json" \
-d '{"groups": ["tts", "twitch"], "expires_in": 900}'
{ "url": "wss://ws.tippage.com", "token": "eyJ...",
"expires_at": "2026-09-02T09:15:00.000Z", "channels": ["dev:t_abc123:tts", "dev:t_abc123:twitch"] }
The client connects with { "data": { "token": "..." } }. The token can only open a stream, must be used within expires_in (60-3600s, default 900), and the socket then stays up past it - a reconnect needs a fresh token. groups is optional and can only narrow what the key’s scopes grant (group_not_granted otherwise).
Channels and scopes
Each group your scopes allow becomes a channel dev:<tenant>:<group>:
| Group | Granted by | Events |
|---|---|---|
tts |
tts:read |
tip.*, tipping.*, queue.tts.* |
media |
media:read |
media.created, queue.media.* |
twitch |
tts:read or channel_points:manage |
twitch.*, alert.finished |
kick |
tts:read |
kick.*, alert.finished |
chat |
commands:manage or chat:write |
chat.command, chat.moderated, character.* - both platforms, data.platform says which |
counters |
counters:manage |
counter.updated |
overlays |
overlays:manage |
overlay.connected, overlay.disconnected |
Switch on event.type. Payloads are in the event catalog and at GET /v1/events.
alert.finished is realtime only, never a webhook: the alert box has stopped showing an alert (it played or was skipped), on the channel of the alert’s platform, with data.event_id, data.platform, data.outcome (played | skipped) and data.alert - the row as GET /v1/alerts/history lists it, so a feed can add it without a read.
overlay.connected means the overlay’s realtime connection is up, not just the page loaded. Disconnects arrive after a short grace window (a clean close in ~5-10s, a crash in ~10-25s), so a reload doesn’t fire one.
Other hosts
wss://dev-ws.tippage.comis an old alias of this endpoint and still works.wss://realtime.uk-london-1.platform.tippage.comis TipPage’s internal overlay transport. It changes without notice - don’t build on it or imitate an overlay. To replace the overlay, use this stream plus claim and finish.