Examples
A JavaScript client with keepalive and reconnect, and a streamer.bot setup that posts new suggestions to Twitch chat.
Both examples follow the rules in Authentication and WebSocket connection. They send the key in the first frame, skip empty keepalive frames, send a text frame every 25 seconds so the server does not drop them, and reconnect on failure.
Warning
Never put your API key in a URL. The server answers any request with a token query parameter with HTTP 401.
JavaScript
This client uses the standard WebSocket class. It runs in a browser and in Node.js 22 or later. In older Node versions, use the ws package, which has the same onopen, onmessage and onclose handlers.
const URL = "wss://YOUR_HOST/api/ws";
const API_KEY = "YOUR_64_CHARACTER_KEY";
const KEEPALIVE_MS = 25_000; // the server drops clients silent for 90 s
const MAX_BACKOFF_MS = 30_000;
let backoff = 1_000;
let keepalive = null;
function connect() {
const socket = new WebSocket(URL);
socket.onopen = () => {
// Must be the first frame, sent within 10 seconds.
socket.send(JSON.stringify({ type: "authenticate", apiKey: API_KEY }));
// Any text frame resets the server's 90 s timer. The server ignores the content.
keepalive = setInterval(() => {
if (socket.readyState === WebSocket.OPEN) {
socket.send(JSON.stringify({ type: "ping" }));
}
}, KEEPALIVE_MS);
};
socket.onmessage = (message) => {
// The server sends an empty text frame every 30 s. Skip it.
if (typeof message.data !== "string" || message.data.length === 0) return;
backoff = 1_000; // a live stream of data means the connection works
handleEvent(JSON.parse(message.data));
};
socket.onclose = (closeEvent) => {
clearInterval(keepalive);
const keyIsDead =
(closeEvent.code === 1008 && closeEvent.reason === "Authentication failed.") ||
(closeEvent.code === 1000 &&
(closeEvent.reason === "API disabled" ||
closeEvent.reason === "API key regenerated"));
if (keyIsDead) {
console.error("Key refused. Check Dashboard > API, then restart this client.");
return; // do not retry
}
console.warn(`Closed (${closeEvent.code}). Reconnecting in ${backoff} ms.`);
setTimeout(connect, backoff);
backoff = Math.min(backoff * 2, MAX_BACKOFF_MS);
};
socket.onerror = () => {
// onclose always follows. Reconnect there.
};
}
function handleEvent(event) {
switch (event.type) {
case "suggestion.submitted":
console.log(`${event.viewer.displayName} suggested ${event.game.title}`);
break;
case "vote.cast":
console.log(
`${event.viewer.displayName} voted ${event.vote.value} on ${event.game.title} ` +
`(net ${event.votes.netVotes})`
);
break;
case "vote.retracted":
console.log(`${event.viewer.displayName} removed a vote on ${event.game.title}`);
break;
case "board_game.moved":
console.log(`${event.game.title} moved to ${event.toSection.name}`);
break;
case "section.created":
case "section.updated":
// outcome is a number: 0 neutral, 1 positive, 2 negative
console.log(`Section ${event.section.name}, outcome ${event.section.outcome}`);
break;
default:
console.log("Unhandled event", event.type);
}
}
connect();
Notes on this client:
- It reads
toSection.name, the name of the section a game entered. - It compares
section.outcomeas a number. - It resets the wait only after a real event arrives. A connection that opens and then fails authentication keeps backing off.
- Only run one copy. A second authenticated connection takes over, and the first goes quiet without closing.
- In a browser, do not host this on a public page with the key in the source. Anyone can read it.
streamer.bot
This setup posts a Twitch chat message when a viewer suggests a game. It uses a WebSocket client in streamer.bot and three actions. One authenticates, one keeps the connection alive on a timer, and one handles events.
Note
The streamer.bot menu names and the name of the argument that holds the message text can differ between versions. If something below does not match your version, open the action's history after a test message to see the arguments it received. See the streamer.bot documentation for its client.
Set up the client
- In streamer.bot, open Servers/Clients, then WebSocket Clients, and add a client.
- Set the endpoint to
wss://YOUR_HOST/api/ws. Do not add any query string. - Turn on connecting at startup and automatic reconnect.
- Save, and note the client's number. The first client is
0. The code below uses0.
Action 1: authenticate when the socket opens
Create an action named NextQuest Auth. Add the trigger WebSocket Client > Open for your client. Add an Execute C# Code sub-action:
using System;
public class CPHInline
{
public bool Execute()
{
string apiKey = "YOUR_64_CHARACTER_KEY";
CPH.WebsocketSend("{\"type\":\"authenticate\",\"apiKey\":\"" + apiKey + "\"}", 0);
return true;
}
}
Streamer.bot runs this on every open, including automatic reconnects. Each new socket must authenticate again, so this is correct.
Action 2: keep the connection alive
Streamer.bot only listens, so the server would drop it after 90 seconds of silence. Create an action named NextQuest Keepalive. Add a Timed Action trigger that runs every 30 seconds, and this code:
using System;
public class CPHInline
{
public bool Execute()
{
if (CPH.WebsocketIsConnected(0))
{
CPH.WebsocketSend("{\"type\":\"ping\"}", 0);
}
return true;
}
}
Tip
If the timer fires in the instant between the socket opening and your auth frame, the server reads the ping as a bad login and closes. The next reconnect fixes it. A 30 second timer makes this rare.
Action 3: post suggestions to chat
Create an action named NextQuest Event. Add the trigger WebSocket Client > Message for your client. Add an Execute C# Code sub-action:
using System;
using Newtonsoft.Json.Linq;
public class CPHInline
{
public bool Execute()
{
// The server sends an empty frame every 30 seconds. Ignore it.
string raw = args.ContainsKey("data") ? Convert.ToString(args["data"]) : "";
if (string.IsNullOrWhiteSpace(raw)) return true;
JObject evt = JObject.Parse(raw);
string type = (string)evt["type"];
if (type == "suggestion.submitted")
{
string viewer = (string)evt["viewer"]["displayName"];
string title = (string)evt["game"]["title"];
CPH.SendMessage(viewer + " suggested " + title + ".", true);
}
else if (type == "vote.cast")
{
string viewer = (string)evt["viewer"]["displayName"];
string game = (string)evt["game"]["title"];
string value = (string)evt["vote"]["value"]; // "up" or "down"
CPH.SendMessage(viewer + " voted " + value + " on " + game + ".", true);
}
else if (type == "board_game.moved")
{
string title = (string)evt["game"]["title"];
string toSection = (string)evt["toSection"]["name"];
CPH.SendMessage(title + " moved to " + toSection + ".", true);
}
return true;
}
}
This posts, for example, CoolViewer42 suggested Hollow Knight.
Things to know:
suggestion.submittedfires each time a viewer adds to a pending suggestion, so the same game can produce several chat lines.vote.castalso fires when a viewer switches from up to down.vote.valueis the new value.- Check for the
typein every message. Your action receives all events. - Store the key in a streamer.bot global variable instead of the action text, so it does not travel with exported actions.
If the connection will not stay up, see the troubleshooting page.