Moved 9 documentation .md files from root into docs/. Moved 4 test scripts from root into tests/. Updated cross-references in README.md and docs to reflect new paths. Co-authored-by: Cursor <cursoragent@cursor.com>
18 KiB
Bot Integration Guide
This guide explains how to integrate your bot with the Jackbox Game Picker API for live voting and game notifications.
Table of Contents
Live Voting (Bot → API)
Your bot can send real-time votes to the API when it detects "thisgame++" or "thisgame--" in Kosmi chat.
Endpoint
POST /api/votes/live
Authentication
Requires JWT token in Authorization header:
Authorization: Bearer YOUR_JWT_TOKEN
Request Body
{
"username": "string", // Username of the voter
"vote": "up" | "down", // "up" for thisgame++, "down" for thisgame--
"timestamp": "string" // ISO 8601 timestamp (e.g., "2025-11-01T20:30:00Z")
}
Response (Success)
{
"success": true,
"message": "Vote recorded successfully",
"session": {
"id": 123,
"games_played": 5
},
"game": {
"id": 45,
"title": "Fibbage 4",
"upvotes": 46,
"downvotes": 3,
"popularity_score": 43
},
"vote": {
"username": "TestUser",
"type": "up",
"timestamp": "2025-11-01T20:30:00Z"
}
}
Error Responses
- 400 Bad Request: Invalid payload or timestamp format
- 404 Not Found: No active session or timestamp doesn't match any game
- 409 Conflict: Duplicate vote (within 1 second of previous vote from same user)
- 500 Internal Server Error: Server error
Example Implementation (Node.js)
// When bot detects "thisgame++" or "thisgame--" in Kosmi chat
async function handleVote(username, message) {
const isUpvote = message.includes('thisgame++');
const isDownvote = message.includes('thisgame--');
if (!isUpvote && !isDownvote) return;
try {
const response = await fetch('http://your-api-url/api/votes/live', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.JWT_TOKEN}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
username: username,
vote: isUpvote ? 'up' : 'down',
timestamp: new Date().toISOString()
})
});
const data = await response.json();
if (response.ok) {
console.log(`Vote recorded for ${data.game.title}: ${data.game.upvotes}👍 ${data.game.downvotes}👎`);
} else {
console.error('Vote failed:', data.error);
}
} catch (error) {
console.error('Error sending vote:', error);
}
}
Important Notes
- Deduplication: Votes from the same user within 1 second are automatically rejected to prevent spam
- Timestamp Matching: The API matches the vote timestamp to the correct game based on when games were played
- Active Session Required: Votes can only be recorded when there's an active session with games played
Game Notifications (API → Bot)
The API can notify your bot when games are added to a session, allowing you to announce "Coming up next: Game Title!" in Kosmi chat.
There are two integration methods available:
- WebSocket (Recommended): Real-time bidirectional communication, simpler setup, works through firewalls
- Webhooks: Traditional HTTP callbacks, good for serverless/stateless integrations
WebSocket Integration (Recommended)
WebSocket provides real-time event streaming from the API to your bot. This is the recommended approach as it:
- Works through firewalls and NAT (bot initiates connection)
- No need to expose inbound ports
- Automatic reconnection on disconnect
- Lower latency than webhooks
- Bidirectional communication
Connection Flow
- Bot connects to WebSocket endpoint
- Bot authenticates with JWT token
- Bot subscribes to active session
- Bot receives
game.addedevents in real-time
WebSocket Endpoint
wss://your-api-url/api/sessions/live
Message Protocol
All messages are JSON-formatted.
Client → Server Messages:
// 1. Authenticate (first message after connecting)
{
"type": "auth",
"token": "YOUR_JWT_TOKEN"
}
// 2. Subscribe to a session
{
"type": "subscribe",
"sessionId": 123
}
// 3. Unsubscribe from a session
{
"type": "unsubscribe",
"sessionId": 123
}
// 4. Heartbeat (keep connection alive)
{
"type": "ping"
}
Server → Client Messages:
// Authentication success
{
"type": "auth_success",
"message": "Authenticated successfully"
}
// Authentication failure
{
"type": "auth_error",
"message": "Invalid or expired token"
}
// Subscription confirmed
{
"type": "subscribed",
"sessionId": 123,
"message": "Subscribed to session 123"
}
// Game added event
{
"type": "game.added",
"timestamp": "2025-11-01T20:30:00Z",
"data": {
"session": {
"id": 123,
"is_active": true,
"games_played": 5
},
"game": {
"id": 45,
"title": "Fibbage 4",
"pack_name": "The Jackbox Party Pack 9",
"min_players": 2,
"max_players": 8,
"manually_added": false,
"room_code": "JYET"
}
}
}
// Session started event (broadcast to all authenticated clients)
{
"type": "session.started",
"timestamp": "2025-11-01T20:00:00Z",
"data": {
"session": {
"id": 123,
"is_active": true,
"created_at": "2025-11-01T20:00:00Z",
"notes": "Friday game night"
}
}
}
// Session ended event (broadcast to session subscribers)
{
"type": "session.ended",
"timestamp": "2025-11-01T23:00:00Z",
"data": {
"session": {
"id": 123,
"is_active": false,
"games_played": 8
}
}
}
// Game started event (broadcast to session subscribers)
// Fired when the Jackbox room becomes locked, meaning gameplay has begun
{
"type": "game.started",
"timestamp": "2025-11-01T20:31:00Z",
"data": {
"sessionId": 123,
"gameId": 456,
"roomCode": "JYET",
"maxPlayers": 8
}
}
// Audience joined event (broadcast to session subscribers)
// Confirms the app successfully joined a Jackbox room as an audience member
{
"type": "audience.joined",
"timestamp": "2025-11-01T20:31:05Z",
"data": {
"sessionId": 123,
"gameId": 456,
"roomCode": "JYET"
}
}
// Heartbeat response
{
"type": "pong"
}
// Error
{
"type": "error",
"message": "Error description"
}
Example Implementation (Node.js)
const WebSocket = require('ws');
class JackboxWebSocketClient {
constructor(apiURL, jwtToken) {
this.apiURL = apiURL.replace(/^http/, 'ws') + '/api/sessions/live';
this.jwtToken = jwtToken;
this.ws = null;
this.reconnectDelay = 1000;
this.maxReconnectDelay = 30000;
}
connect() {
this.ws = new WebSocket(this.apiURL);
this.ws.on('open', () => {
console.log('WebSocket connected');
this.authenticate();
this.startHeartbeat();
});
this.ws.on('message', (data) => {
this.handleMessage(JSON.parse(data));
});
this.ws.on('close', () => {
console.log('WebSocket disconnected, reconnecting...');
this.reconnect();
});
this.ws.on('error', (err) => {
console.error('WebSocket error:', err);
});
}
authenticate() {
this.send({ type: 'auth', token: this.jwtToken });
}
subscribe(sessionId) {
this.send({ type: 'subscribe', sessionId });
}
send(message) {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify(message));
}
}
handleMessage(message) {
switch (message.type) {
case 'auth_success':
console.log('Authenticated successfully');
// Get active session and subscribe
this.getActiveSessionAndSubscribe();
break;
case 'auth_error':
console.error('Authentication failed:', message.message);
break;
case 'subscribed':
console.log('Subscribed to session:', message.sessionId);
break;
case 'game.added':
this.handleGameAdded(message.data);
break;
case 'pong':
// Heartbeat response
break;
case 'error':
console.error('Server error:', message.message);
break;
}
}
handleGameAdded(data) {
const { game } = data;
// Build announcement with room code if available
let announcement = `🎮 Coming up next: ${game.title}!`;
if (game.room_code) {
announcement += ` Join at jackbox.tv with code: ${game.room_code}`;
}
// Send to your chat platform (e.g., Kosmi chat)
this.broadcastToChat(announcement);
}
startHeartbeat() {
setInterval(() => {
this.send({ type: 'ping' });
}, 30000); // Every 30 seconds
}
reconnect() {
setTimeout(() => {
this.connect();
this.reconnectDelay = Math.min(this.reconnectDelay * 2, this.maxReconnectDelay);
}, this.reconnectDelay);
}
async getActiveSessionAndSubscribe() {
// Fetch active session from REST API
const response = await fetch(`${this.apiURL.replace('/api/sessions/live', '')}/api/sessions/active`, {
headers: { 'Authorization': `Bearer ${this.jwtToken}` }
});
if (response.ok) {
const session = await response.json();
if (session && session.id) {
this.subscribe(session.id);
}
}
}
broadcastToChat(message) {
// Implement your chat platform integration here
console.log('Broadcasting:', message);
}
}
// Usage
const client = new JackboxWebSocketClient('https://your-api-url', 'YOUR_JWT_TOKEN');
client.connect();
Example Implementation (Go)
See the reference implementation in irc-kosmi-relay/bridge/jackbox/websocket_client.go.
Webhook Integration
Webhooks are HTTP callbacks sent from the API to your bot when events occur. This is an alternative to WebSocket for bots that prefer stateless integrations.
Webhook Event: game.added
Triggered whenever a game is added to an active session (either via picker or manual selection).
Webhook Payload
{
"event": "game.added",
"timestamp": "2025-11-01T20:30:00Z",
"data": {
"session": {
"id": 123,
"is_active": true,
"games_played": 5
},
"game": {
"id": 45,
"title": "Fibbage 4",
"pack_name": "The Jackbox Party Pack 9",
"min_players": 2,
"max_players": 8,
"manually_added": false,
"room_code": "JYET"
}
}
}
Note:
room_codeis the 4-character Jackbox room code (e.g."JYET"). It will benullif no room code was provided when the game was added.
Webhook Headers
The API sends the following headers with each webhook:
Content-Type: application/jsonX-Webhook-Signature: sha256=<hmac_signature>- HMAC-SHA256 signature for verificationX-Webhook-Event: game.added- Event typeUser-Agent: Jackbox-Game-Picker-Webhook/1.0
Signature Verification
IMPORTANT: Always verify the webhook signature to ensure the request is authentic.
const crypto = require('crypto');
function verifyWebhookSignature(signature, payload, secret) {
if (!signature || !signature.startsWith('sha256=')) {
return false;
}
const expectedSignature = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(JSON.stringify(payload))
.digest('hex');
// Use timing-safe comparison
try {
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
} catch (err) {
return false;
}
}
Example Implementation (Express.js)
const express = require('express');
const crypto = require('crypto');
const app = express();
// IMPORTANT: Use express.json() with verify option to get raw body
app.use(express.json({
verify: (req, res, buf) => {
req.rawBody = buf.toString('utf8');
}
}));
app.post('/webhook/jackbox', (req, res) => {
const signature = req.headers['x-webhook-signature'];
const secret = process.env.WEBHOOK_SECRET; // Your webhook secret
// Verify signature
if (!signature || !signature.startsWith('sha256=')) {
return res.status(401).send('Missing or invalid signature');
}
const expectedSignature = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(req.rawBody)
.digest('hex');
// Timing-safe comparison
try {
if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expectedSignature))) {
return res.status(401).send('Invalid signature');
}
} catch (err) {
return res.status(401).send('Invalid signature');
}
// Handle the event
if (req.body.event === 'game.added') {
const game = req.body.data.game;
// Build announcement with room code if available
let message = `🎮 Coming up next: ${game.title}!`;
if (game.room_code) {
message += ` Join at jackbox.tv with code: ${game.room_code}`;
}
// Send message to Kosmi chat
sendKosmiMessage(message);
console.log(`Announced game: ${game.title} from ${game.pack_name} (code: ${game.room_code || 'N/A'})`);
}
// Always respond with 200 OK
res.status(200).send('OK');
});
function sendKosmiMessage(message) {
// Your Kosmi chat integration here
console.log('Sending to Kosmi:', message);
}
app.listen(3001, () => {
console.log('Webhook receiver listening on port 3001');
});
Webhook Management
You can manage webhooks through the API using the following endpoints (all require JWT authentication).
List All Webhooks
GET /api/webhooks
Authorization: Bearer YOUR_JWT_TOKEN
Create Webhook
POST /api/webhooks
Authorization: Bearer YOUR_JWT_TOKEN
Content-Type: application/json
{
"name": "Kosmi Bot",
"url": "http://your-bot-url/webhook/jackbox",
"secret": "your_shared_secret_key",
"events": ["game.added"]
}
Update Webhook
PATCH /api/webhooks/:id
Authorization: Bearer YOUR_JWT_TOKEN
Content-Type: application/json
{
"enabled": false // Disable webhook
}
Delete Webhook
DELETE /api/webhooks/:id
Authorization: Bearer YOUR_JWT_TOKEN
Test Webhook
POST /api/webhooks/test/:id
Authorization: Bearer YOUR_JWT_TOKEN
Sends a test game.added event to verify your webhook is working.
View Webhook Logs
GET /api/webhooks/:id/logs?limit=50
Authorization: Bearer YOUR_JWT_TOKEN
Returns recent webhook delivery attempts with status codes and errors.
Testing
Test Live Voting
# Get your JWT token first
curl -X POST "http://localhost:5000/api/auth/login" \
-H "Content-Type: application/json" \
-d '{"apiKey": "YOUR_API_KEY"}'
# Send a test vote
curl -X POST "http://localhost:5000/api/votes/live" \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"username": "TestUser",
"vote": "up",
"timestamp": "2025-11-01T20:30:00Z"
}'
Test Webhooks
# Create a webhook
curl -X POST "http://localhost:5000/api/webhooks" \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Test Webhook",
"url": "http://localhost:3001/webhook/jackbox",
"secret": "test_secret_123",
"events": ["game.added"]
}'
# Test the webhook
curl -X POST "http://localhost:5000/api/webhooks/test/1" \
-H "Authorization: Bearer YOUR_JWT_TOKEN"
# Check webhook logs
curl -X GET "http://localhost:5000/api/webhooks/1/logs" \
-H "Authorization: Bearer YOUR_JWT_TOKEN"
Available Events
Webhook Events
game.added- Triggered when a game is added to an active session. Includesroom_code(the 4-character Jackbox join code) if one was provided.
WebSocket Events
game.added- Triggered when a game is added to an active session. Sent to clients subscribed to that session. Includesroom_code.session.started- Triggered when a new session is created. Broadcast to all authenticated clients (no subscription required).session.ended- Triggered when a session is closed. Sent to clients subscribed to that session.game.started- Triggered when the Jackbox room becomes locked (gameplay has begun). Detected by polling the Jackbox REST API every 10 seconds. Sent to clients subscribed to that session. IncludesroomCodeandmaxPlayers.audience.joined- Triggered when the app successfully joins a Jackbox room as an audience member. Sent to clients subscribed to that session. This confirms the room code is valid and the game is being monitored.player-count.updated- Triggered when the player count for a game is updated. Sent to clients subscribed to that session.
Tip: To receive
session.startedevents, your bot only needs to authenticate — no subscription is needed. Once you receive asession.startedevent, subscribe to the new session ID to receivegame.addedandsession.endedevents for it.
More events may be added in the future (e.g., vote.recorded).
Security Best Practices
- Always verify webhook signatures - Never trust webhook payloads without verification
- Use HTTPS in production - Webhook URLs should use HTTPS to prevent man-in-the-middle attacks
- Keep secrets secure - Store webhook secrets in environment variables, never in code
- Implement rate limiting - Protect your webhook endpoints from abuse
- Log webhook activity - Keep logs of webhook deliveries for debugging
- Use strong secrets - Generate cryptographically secure random strings for webhook secrets
Troubleshooting
Votes Not Being Recorded
- Check that there's an active session with games played
- Verify the timestamp is within the timeframe of a played game
- Ensure you're not sending duplicate votes within 1 second
- Check API logs for error messages
Webhooks Not Being Received
- Verify your webhook URL is publicly accessible
- Check webhook logs via
/api/webhooks/:id/logs - Test with
ngrokor similar tool if developing locally - Ensure your webhook endpoint responds with 200 OK
- Check that webhook is enabled in the database
Signature Verification Failing
- Ensure you're using the raw request body for signature verification
- Check that the secret matches what's stored in the database
- Verify you're using HMAC-SHA256 algorithm
- Make sure to prefix with "sha256=" when comparing
Support
For issues or questions, contact: cottongin@cottongin.xyz