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) · 4k

A 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.