Skip to content
TipPage Docs
Esc
navigateopen⌘Jpreview
On this page

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.

Everything the webhooks deliver is also available as a live stream over a WebSocket at wss://ws.tippage.com. Same events, same payloads, same event ids - the only difference is delivery:

  • Webhooks are durable. We retry for ~44 hours until your endpoint accepts them, so you never miss one - at the cost of running a public HTTPS endpoint.
  • The realtime stream is live and ephemeral. You receive what happens while you’re connected and nothing you missed while you weren’t - at the cost of nothing: open a socket and go.

Reach for the stream when you react now and don’t care about history - a Stream Deck plugin, a “now playing” panel, a light rig, your own overlay. Reach for webhooks when an event must never be lost - logging, VOD markers, bookkeeping. Running both is fine: an event carries the same id on both, so deduping is a one-line Set check.

Connecting

No SDK, no protocol library, no dependencies - any WebSocket client in any language works. Open the socket, send one JSON line with your credential, and events stream in:

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; // skip the connect frame and the {} keepalives
  const event = frame.pub.data; // the exact envelope a webhook would POST
  console.log(event.type, event.data); // e.g. "tip.created", { name, amount, ... }
};

Three frame shapes come down the wire, all JSON, one object per message:

// 1. First frame - you're connected. `subs` lists the channels your key's
//    scopes granted. You're already subscribed; there's nothing to send back.
{ "connect": { "client": "...", "subs": { "dev:t_abc123:tts": {} }, "ping": 25 } }

// 2. An event - the webhook envelope, wrapped with the channel it came in on.
{ "channel": "dev:t_abc123:tts",
  "pub": { "data": { "id": "evt_...", "type": "tip.created", "created": "...", "actor": null, "data": { ... } } } }

// 3. Keepalive, about every 25s. Ignore it - or treat a long gap between
//    keepalives as a dead connection and reconnect.
{}

That’s the entire protocol. The stream is receive-only: after the first message the server never expects another word from you. Reconnect with backoff if the socket closes - a close code in the 4500-4999 range means the credential needs fixing (see Lifecycle); anything else is a transient blip, just dial back in.

Choosing your credential

There are two ways to authenticate the socket, and which you pick comes down to one question: does the thing opening the connection get to see your API key?

Your API key - for connections you control

When your own backend, a server-side worker, or a private script opens the socket, send your key directly:

{ "data": { "apiKey": "tp_live_..." } }

This is the simple path for first-party use. The key stays in your environment, never leaves your infrastructure, and the connection stays up as long as the key is valid - reconnect with the same key forever, no refresh, nothing to manage. Use this whenever you can keep the key secret.

Never ship your key to a browser, a desktop app you distribute, a viewer’s device, or anyone else’s server. A leaked tp_live_ key is full API access until you rotate it.

A handoff token - to let other clients connect without your key

Sometimes you want connections you don’t control to receive your events: a browser dashboard, a mobile app, a viewer’s page, a customer’s integration. You can’t give them your key. Instead, your server mints them a short-lived handoff token and hands that over:

curl -X POST https://api.tippage.com/v1/realtime/token \
  -H "Authorization: Bearer tp_live_..." \
  -H "Content-Type: application/json" \
  -d '{"expires_in": 900}'

The flow is:

  1. A client asks your server for access (however you gate that - login, session, API key of your own, whatever).
  2. Your server calls POST /v1/realtime/token with your TipPage key and gets back a token.
  3. Your server hands the token to the client.
  4. The client connects with { "data": { "token": "..." } } and receives your events.

Your key never leaves your server. The client only ever holds a token that expires, that can only open an event stream (it can’t call any other API endpoint), and that grants exactly the channels your key’s scopes allow - nothing more. This is what makes browsers safe: a page can hold a handoff token; it must never hold your key.

The token is a connect credential only. It must be used within expires_in (60-3600 seconds, default 900), but once connected the socket behaves identically to an API-key connection and stays up long past the token’s expiry. If that socket later drops, the client needs a fresh token to reconnect - so mint them on demand rather than far ahead of time. (Daemons that hold the key don’t need tokens at all - they reconnect with the key and never expire.)

Narrowing a token to fewer groups. By default a token grants everything the key’s scopes allow. Pass groups to hand out a token that sees only some of them - so a broad key (even a super-admin one) can give a client a token limited to, say, tips and Twitch activity and nothing else:

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}'

A group is the short name from the channel table below (tts, media, twitch, kick, chat, counters, overlays) - the part that becomes the channel dev:<your-tenant>:<group> on the wire. groups can only narrow: every name must be one the key’s own scopes already grant, or the request is rejected (group_not_granted). The key’s live scopes stay the ceiling at connect time too, so a token can never widen itself.

The POST /v1/realtime/token response tells you which channels the token will actually grant (after any narrowing), so your server can show a client what it’s about to receive:

{
  "url": "wss://ws.tippage.com",
  "token": "eyJ...",
  "expires_at": "2026-09-02T09:15:00.000Z",
  "channels": ["dev:t_abc123:tts", "dev:t_abc123:twitch"]
}

Channels and scopes

Events are organised into groups - tts, media, twitch, and so on. Each group your key’s scopes allow becomes a channel dev:<your-tenant>:<group> that your connection is subscribed to. The server derives the set from your scopes: a handoff token can be narrowed to fewer groups (see above), never widened past what the scopes grant. Connecting (or minting a token) needs at least one group; a key with none is refused with close code 4507.

Group Wire channel Granted by Events
tts dev:<tenant>:tts tts:read tip.*, tipping.*, queue.tts.*
media dev:<tenant>:media media:read media.created, queue.media.*
twitch dev:<tenant>:twitch tts:read or channel_points:manage twitch.*
kick dev:<tenant>:kick tts:read kick.*
chat dev:<tenant>:chat commands:manage or chat:write chat.command, chat.moderated - both platforms; data.platform says which chat
counters dev:<tenant>:counters counters:manage counter.updated
overlays dev:<tenant>:overlays overlays:manage overlay.connected, overlay.disconnected

Switch on event.type in your handler; the channel an event arrived on is mostly there to tell you which group (and scope) let it through. Platforms are split the same way as the webhook catalog: Twitch alerts arrive on twitch as twitch.*, Kick alerts on kick as kick.*, and chat activity from either platform arrives on the single chat group with data.platform set to twitch or kick - there is no per-platform chat channel. The full catalog with payload shapes is on the Webhooks event catalog and at GET /v1/events.

Overlay connection events

overlay.connected and overlay.disconnected tell you when a browser source goes online or offline - handy for a “stream is live” indicator or alerting when an overlay drops mid-stream. Both name the overlay by its public id (an account can have several), and disconnects carry a reason: closed (the page told us it was unloading - OBS closed or the source was removed or hidden with “shutdown when not visible”), socket_closed (its realtime connection dropped and did not come back - a crash, a killed process, lost network), heartbeat_timeout (it went silent without either signal), key_rotated, overlay_deleted, or tenant_reset. Because an overlay is only considered gone after a short grace window, a brief reload or network blip won’t fire a spurious disconnect. A clean close is reported within about 5-10 seconds, a crash within about 10-15 seconds, and a source that just goes silent within about 60-80 seconds.

Lifecycle and revocation

The server re-verifies your key about every 30 seconds. Rotating, disabling, deleting, or re-scoping a key disconnects its live connections within seconds (close code 4508); after a scope change, reconnect and the new channel set applies. Codes in the 4500-4999 range are terminal - fix the credential, then reconnect:

Code Meaning
4501 Credential invalid - bad key, or an expired or malformed handoff token
4507 The key (or token’s key) has no realtime-capable scopes
4508 The key was rotated, disabled, deleted, or re-scoped mid-connection

One caveat, because live means live: while your client is disconnected - a deploy, a crash, a network drop - events fired in that gap are gone from the stream. If you can’t tolerate gaps, back the stream with a webhook endpoint; the shared event id makes reconciling the two trivial.

A note on realtime.uk-london-1.platform.tippage.com

You may notice a second WebSocket host, wss://realtime.uk-london-1.platform.tippage.com. That’s TipPage’s own realtime gateway - the transport our stream overlays and the dashboard run on. It is not a developer surface, and we don’t recommend building on it. We change how the overlay talks to our API and socket server often, without warning and without calling it a breaking change - so anything you connect there today is likely to break later. For anything you depend on, use ws.tippage.com above: it’s the versioned, documented, reliable surface, and it’s covered by the API’s compatibility promise.

Two things follow from that:

  • Don’t imitate an overlay. An overlay’s credentials, channels, and message format are private wiring between our own components - undocumented and unversioned. Reverse-engineering it is the most fragile thing you can build, and when it breaks that won’t count as a regression. If what you want is your own overlay, build it on the documented surfaces: this event stream to react live, plus the claim-and-finish protocol to play tips yourself - the very protocol our overlay uses, exposed properly.

  • The centrifuge library technically works there, but comes with the same warning. The gateway speaks the Centrifugo protocol, so if you’d rather use a client library than the raw ws.tippage.com socket, the centrifuge npm package can connect to it using an overlay key. Treat it as a convenience that may break without warning, never something to build on. We highly recommend you use ws.tippage.com.

Moving from dev-ws.tippage.com

Before 07/09/2026 the stream lived at wss://dev-ws.tippage.com. That hostname still answers - it’s the same endpoint under an alias, so existing integrations keep working - but new code should connect to wss://ws.tippage.com, which is what POST /v1/realtime/token now returns in url.

Was this page helpful?