Quickstarts
Up and running in minutes
Every Realtime feature follows one shape: your server asks the API for a short-lived credential with your secret key, and your app uses it to connect. Your secret key never reaches a phone or a browser.
Chat in five minutes
Rooms over XMPP, the protocol behind WhatsApp-scale chat. Messages are never charged; you pay … per monthly active user after the free allowance.
1Your server gets a credential and a room
Call these from an endpoint your signed-in user hits. The credential lasts hours, not forever, and DELETE /v1/realtime/chat/tokens/{user_id} ends it at once (a ban, a sign-out).
# 1. A chat credential for one of YOUR users (server side, secret key)
curl https://api.gossipbuzz.app/v1/realtime/chat/tokens \
-H "Authorization: Bearer $GOSSIP_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{"user_id": "user-42", "name": "Asha"}'
# 2. The address of a room (the API names rooms, inside your app's space)
curl https://api.gossipbuzz.app/v1/realtime/chat/rooms \
-H "Authorization: Bearer $GOSSIP_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{"room": "lobby"}'You get back ws, domain, user, password and nickname, and the room's address. Room names are namespaced to your app, so two apps can never share a room by accident.
2Your app connects and joins
// npm install @xmpp/client
import { client, xml } from '@xmpp/client'
const cred = await fetch('/chat/join', { method: 'POST' }).then((r) => r.json())
const xmpp = client({
service: cred.ws,
domain: cred.domain,
username: cred.user,
password: cred.password,
})
xmpp.on('stanza', (stanza) => {
if (stanza.is('message') && stanza.attrs.type === 'groupchat') {
console.log(stanza.attrs.from, stanza.getChildText('body'))
}
})
await xmpp.start()
// Join the room (asking for no history), then say something.
await xmpp.send(xml('presence', { to: `${cred.room}/${cred.nickname}` },
xml('x', { xmlns: 'http://jabber.org/protocol/muc' }, xml('history', { maxstanzas: '0' }))))
await xmpp.send(xml('message', { to: cred.room, type: 'groupchat' }, xml('body', {}, 'Hello')))On iOS and Android, any XMPP library that speaks XMPP over WebSocket (RFC 7395) works with the same five values: XMPPFramework on iOS and Smack's WebSocket module on Android are the common choices. Join a room with the same presence, and history is off unless you turn it on for the app.
3Decide whether chat keeps history
Off by default, which suits live-stream chat: nothing said is stored. Turn on Keep chat history for the app in the dashboard's Realtime section and rooms keep an archive users can scroll back through.
Live streams and calls
One-to-many live streams, video calls (1:1 or group) and audio calls, on the open-source LiveKit SFU. Viewers pay by quality: … a minute at 720p, … at 1080p, … at 4K, bandwidth included. Audio calls are … a minute.
1Your server gets a token
curl https://api.gossipbuzz.app/v1/realtime/live/tokens \
-H "Authorization: Bearer $GOSSIP_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{"room": "show-1", "identity": "user-42", "kind": "live", "publish": true, "quality": "fhd"}'
# kind: live (one-to-many) · call (video, 1:1 or group) · audio (voice only)
# publish: true for the broadcaster; viewers leave it false
# quality: hd (720p) · fhd (1080p) · 4kA room that publishes above its declared quality is billed at what it published, so declare the quality you mean. An audio room can publish the microphone and nothing else.
2Your app joins the room
With LiveKit's official SDK for your platform:
// npm install livekit-client
import { Room, RoomEvent } from 'livekit-client'
const { url, token } = await fetch('/live/join', { method: 'POST' }).then((r) => r.json())
const room = new Room()
room.on(RoomEvent.TrackSubscribed, (track) => {
document.getElementById('stage').appendChild(track.attach())
})
await room.connect(url, token)
// The broadcaster (a token with publish: true) turns the camera on:
await room.localParticipant.enableCameraAndMicrophone()Count real viewers
Your backend cannot see a viewer's address; ours can. Your app asks for a ticket, and your server checks it with no call back to us. A script, a headless browser, or one address posing as a crowd gets a ticket that says so. … per check.
1Your app asks for a ticket
With the publishable key from the dashboard, which is safe in a client and grants nothing on its own. subject is what you count (a device), scope is what it is counted against (a stream).
// In the browser, with the PUBLISHABLE key (safe in a client).
const r = await fetch('https://api.gossipbuzz.app/v1/realtime/shield/ticket', {
method: 'POST',
headers: { 'X-Publishable-Key': 'pk_live_…', 'Content-Type': 'application/json' },
body: JSON.stringify({ subject: deviceId, scope: streamId }),
})
const { ticket } = await r.json()
// Send `ticket` to your server with the heartbeat you already send.2Your server checks it
With the app's Shield secret (dashboard → Realtime → Reveal). Count the viewer only when this returns true.
import { createHmac, timingSafeEqual } from 'node:crypto'
// True only for a genuine, unexpired "ok" ticket for THIS viewer on THIS stream.
function checkTicket(ticket, subject, scope, secret) {
const [body, sig] = String(ticket).split('.')
if (!body || !sig) return false
const want = createHmac('sha256', secret).update(body).digest('base64url')
if (sig.length !== want.length || !timingSafeEqual(Buffer.from(sig), Buffer.from(want))) return false
const t = JSON.parse(Buffer.from(body, 'base64url').toString('utf8'))
return t.s === subject && t.c === scope && t.e * 1000 > Date.now() && t.v === 'ok'
}
// In your heartbeat handler:
const counts = checkTicket(req.body.ticket, req.body.deviceId, req.body.streamId, process.env.GOSSIP_SHIELD_SECRET)The verdicts: ok counts; bot is automation by its own user agent; suspicious is one address showing more than 6 viewers of one scope in a day; block is more than 20. The first six devices of a household stay ok all day.
Next
Every endpoint, parameter and response is in the API reference, generated from the API itself. Usage, cost and the request log are in the dashboard.