217 lines
7.1 KiB
Markdown
217 lines
7.1 KiB
Markdown
|
|
# Webhooks and Events
|
|||
|
|
|
|||
|
|
A narrative guide to the Jackbox Game Picker event notification system: webhooks (HTTP callbacks) and WebSocket (persistent real-time connections). Both deliver event data about session and game activity.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 1. Two Notification Systems
|
|||
|
|
|
|||
|
|
The API offers two complementary ways to receive event notifications:
|
|||
|
|
|
|||
|
|
| System | Model | Best for |
|
|||
|
|
|--------|-------|----------|
|
|||
|
|
| **Webhooks** | HTTP POST callbacks to your URL | Server-to-server, external integrations |
|
|||
|
|
| **WebSocket** | Persistent bidirectional connection | Real-time UIs, dashboards, live tools |
|
|||
|
|
|
|||
|
|
Both systems emit the same kinds of events (e.g. `game.added`) but differ in how they deliver them.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 2. When to Use Which
|
|||
|
|
|
|||
|
|
### Use Webhooks when:
|
|||
|
|
|
|||
|
|
- **Server-to-server** — Discord bots, Slack, logging pipelines, external APIs
|
|||
|
|
- **Stateless** — Your endpoint receives a POST, processes it, and returns. No long-lived connection
|
|||
|
|
- **Behind firewalls** — Your server can receive HTTP but may not hold open WebSocket connections
|
|||
|
|
- **Async delivery** — You’re fine with HTTP round-trip latency and want delivery logged and auditable
|
|||
|
|
|
|||
|
|
### Use WebSocket when:
|
|||
|
|
|
|||
|
|
- **Real-time UI** — Dashboards, admin panels, live session viewers
|
|||
|
|
- **Instant updates** — You need push-style notifications with minimal latency
|
|||
|
|
- **Persistent connection** — Your app keeps a live connection and subscribes to specific sessions
|
|||
|
|
- **Best-effort is fine** — WebSocket is push-only; there’s no built-in delivery log for events
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 3. Webhook Setup
|
|||
|
|
|
|||
|
|
Webhooks are registered via the REST API. See [Webhooks endpoints](../endpoints/webhooks.md) for full CRUD details.
|
|||
|
|
|
|||
|
|
### Create a Webhook
|
|||
|
|
|
|||
|
|
`POST /api/webhooks` with:
|
|||
|
|
|
|||
|
|
- `name` — Display name (e.g. `"Discord Bot"`)
|
|||
|
|
- `url` — Callback URL (must be a valid HTTP/HTTPS URL)
|
|||
|
|
- `secret` — Shared secret for signing payloads (HMAC-SHA256)
|
|||
|
|
- `events` — Array of event types that trigger this webhook (e.g. `["game.added"]`)
|
|||
|
|
|
|||
|
|
**Example:**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
curl -X POST "http://localhost:5000/api/webhooks" \
|
|||
|
|
-H "Authorization: Bearer $TOKEN" \
|
|||
|
|
-H "Content-Type: application/json" \
|
|||
|
|
-d '{
|
|||
|
|
"name": "Discord Bot",
|
|||
|
|
"url": "https://my-server.com/webhooks/jackbox",
|
|||
|
|
"secret": "mysecret123",
|
|||
|
|
"events": ["game.added"]
|
|||
|
|
}'
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
The `events` array defines which events fire this webhook. Currently, the codebase triggers webhooks for **`game.added`** when a game is added to a session. The `triggerWebhook` function in `backend/utils/webhooks.js` is invoked from `sessions.js` on that event.
|
|||
|
|
|
|||
|
|
### Update, Enable/Disable, Delete
|
|||
|
|
|
|||
|
|
- **Update:** `PATCH /api/webhooks/{id}` — Change `name`, `url`, `secret`, `events`, or `enabled`
|
|||
|
|
- **Disable:** `PATCH /api/webhooks/{id}` with `"enabled": false` — Stops delivery without deleting config
|
|||
|
|
- **Delete:** `DELETE /api/webhooks/{id}` — Removes webhook and its logs
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 4. Webhook Delivery
|
|||
|
|
|
|||
|
|
### How it works
|
|||
|
|
|
|||
|
|
When an event occurs (e.g. a game is added), the server:
|
|||
|
|
|
|||
|
|
1. Finds all enabled webhooks subscribed to that event
|
|||
|
|
2. Sends an async HTTP POST to each webhook URL
|
|||
|
|
3. Logs each delivery attempt in `webhook_logs` (status, error, payload)
|
|||
|
|
|
|||
|
|
### Payload format
|
|||
|
|
|
|||
|
|
Each POST body is JSON:
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"event": "game.added",
|
|||
|
|
"timestamp": "2026-03-15T20:30:00.000Z",
|
|||
|
|
"data": {
|
|||
|
|
"session": { "id": 3, "is_active": true, "games_played": 2 },
|
|||
|
|
"game": {
|
|||
|
|
"id": 42,
|
|||
|
|
"title": "Quiplash 3",
|
|||
|
|
"pack_name": "Jackbox Party Pack 7",
|
|||
|
|
"min_players": 3,
|
|||
|
|
"max_players": 8,
|
|||
|
|
"manually_added": false
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Headers include:
|
|||
|
|
|
|||
|
|
- `Content-Type: application/json`
|
|||
|
|
- `X-Webhook-Event: game.added`
|
|||
|
|
- `X-Webhook-Signature: sha256=<hmac>` — Use your `secret` to verify the payload
|
|||
|
|
|
|||
|
|
### View delivery logs
|
|||
|
|
|
|||
|
|
`GET /api/webhooks/{id}/logs` returns recent delivery attempts (status, error message, payload).
|
|||
|
|
|
|||
|
|
### Test a webhook
|
|||
|
|
|
|||
|
|
`POST /api/webhooks/test/{id}` sends a dummy `game.added` event to the webhook URL. Delivery runs asynchronously; check logs for status.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 5. WebSocket Events
|
|||
|
|
|
|||
|
|
The WebSocket server runs at `/api/sessions/live` on the same host and port as the HTTP API. See [WebSocket protocol](../websocket.md) for connection, authentication, and subscription details.
|
|||
|
|
|
|||
|
|
### Event types and audience
|
|||
|
|
|
|||
|
|
| Event | Broadcast to | Triggered by |
|
|||
|
|
|-------|--------------|--------------|
|
|||
|
|
| `session.started` | All authenticated clients | `POST /api/sessions` |
|
|||
|
|
| `game.added` | Session subscribers | `POST /api/sessions/{id}/games` |
|
|||
|
|
| `session.ended` | Session subscribers | `POST /api/sessions/{id}/close` |
|
|||
|
|
| `player-count.updated` | Session subscribers | `PATCH /api/sessions/{sessionId}/games/{sessionGameId}/player-count` |
|
|||
|
|
|
|||
|
|
`session.started` goes to every authenticated client. The others go only to clients that have subscribed to the relevant session via `{ "type": "subscribe", "sessionId": 3 }`.
|
|||
|
|
|
|||
|
|
### Envelope format
|
|||
|
|
|
|||
|
|
All events use this envelope:
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"type": "<event-type>",
|
|||
|
|
"timestamp": "2026-03-15T20:30:00.000Z",
|
|||
|
|
"data": { ... }
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
`data` contains event-specific fields (session, game, player count, etc.) as described in [WebSocket protocol](../websocket.md).
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 6. Comparison
|
|||
|
|
|
|||
|
|
| Feature | Webhooks | WebSocket |
|
|||
|
|
|---------|----------|-----------|
|
|||
|
|
| **Connection** | Stateless HTTP | Persistent |
|
|||
|
|
| **Auth** | Secret in config | JWT per connection |
|
|||
|
|
| **Events** | `game.added` | `session.started`, `game.added`, `session.ended`, `player-count.updated` |
|
|||
|
|
| **Latency** | Higher (HTTP round trip) | Lower (push) |
|
|||
|
|
| **Reliability** | Logged, auditable | Best-effort |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 7. Example: Discord Bot
|
|||
|
|
|
|||
|
|
Use a webhook to post game additions to a Discord channel. You’ll need:
|
|||
|
|
|
|||
|
|
1. A webhook created in the Game Picker API pointing to your server
|
|||
|
|
2. A small server that receives the webhook and forwards to Discord’s Incoming Webhook
|
|||
|
|
|
|||
|
|
**Webhook receiver (Node.js):**
|
|||
|
|
|
|||
|
|
```javascript
|
|||
|
|
const crypto = require('crypto');
|
|||
|
|
|
|||
|
|
app.post('/webhooks/jackbox', express.json(), (req, res) => {
|
|||
|
|
const signature = req.headers['x-webhook-signature'];
|
|||
|
|
const payload = JSON.stringify(req.body);
|
|||
|
|
|
|||
|
|
// Verify HMAC-SHA256 using your webhook secret
|
|||
|
|
const expected = 'sha256=' + crypto
|
|||
|
|
.createHmac('sha256', process.env.WEBHOOK_SECRET)
|
|||
|
|
.update(payload)
|
|||
|
|
.digest('hex');
|
|||
|
|
|
|||
|
|
if (signature !== expected) {
|
|||
|
|
return res.status(401).send('Invalid signature');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (req.body.event === 'game.added') {
|
|||
|
|
const { session, game } = req.body.data;
|
|||
|
|
const discordPayload = {
|
|||
|
|
content: `🎮 **${game.title}** added to session #${session.id} (${game.min_players}-${game.max_players} players)`
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
fetch(process.env.DISCORD_WEBHOOK_URL, {
|
|||
|
|
method: 'POST',
|
|||
|
|
headers: { 'Content-Type': 'application/json' },
|
|||
|
|
body: JSON.stringify(discordPayload)
|
|||
|
|
}).catch(err => console.error('Discord post failed:', err));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
res.status(200).send('OK');
|
|||
|
|
});
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Register the Game Picker webhook with your server’s URL (e.g. `https://my-bot.example.com/webhooks/jackbox`), set `events` to `["game.added"]`, and use the same `secret` in your server’s `WEBHOOK_SECRET`.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Cross-references
|
|||
|
|
|
|||
|
|
- **[Webhooks endpoints](../endpoints/webhooks.md)** — Full CRUD, request/response schemas, errors
|
|||
|
|
- **[WebSocket protocol](../websocket.md)** — Connection, auth, subscriptions, event payloads
|