Webhook Documentation
Learn how to receive real-time updates from TallyDeck widgets and securely authenticate them on your server.
Authentication & Security
When an event triggers in a TallyDeck widget, an HTTP POST request is dispatched to your configured endpoint. We sign all outgoing webhook payloads so you can verify they genuinely originated from our servers.
The Signature Header
Each request includes a Tallydeck-Signature HTTP header containing a Unix timestamp and an HMAC SHA-256 signature.
Tallydeck-Signature: t=1734567890,v1=a1b2c3d4e5f6...
âšī¸ How Signatures Work
The signature (v1) changes on every single request because it hashes both the unique timestamp and the JSON body together. However, your Webhook Secret (the whsec_... string found in your dashboard) is your static environment variable. You never change it. You use your static secret key to perform the HMAC hash locally on your server and see if it mathematically matches our dynamic v1 signature.
Verification Process
To verify the signature in your application backend:
- Extract values: Split the header using the comma to obtain the timestamp (
t) and the signature (v1). - Prevent Replay Attacks: Compare the timestamp
tto your server's current time. Reject the webhook if the timestamp is unreasonably old (e.g., > 5 minutes). - Compute the expected signature:
- Create the signature payload string by concatenating the timestamp, a period (
.), and the exact raw string JSON of the request body:${t}.${raw_body}. - Hash the resulting string using HMAC SHA-256 and your static Webhook Endpoint Secret (
whsec_...). - Compare your computed hexadecimal hash to the provided
v1signature. If they match, the request securely originated from us.
- Create the signature payload string by concatenating the timestamp, a period (
Verification Example (Node.js)
Here is a complete, zero-dependency Node.js example demonstrating how to parse the header, prevent replay attacks, and securely verify the HMAC SHA-256 signature using the built-in crypto module.
const crypto = require('crypto');
// 1. Extract and Parse the Header
// e.g., "t=1734567890,v1=abcdef..."
const signatureHeader = req.headers['tallydeck-signature'];
const parts = signatureHeader.split(',');
const timestamp = parts.find(p => p.startsWith('t=')).split('=')[1];
const receivedSignature = parts.find(p => p.startsWith('v1=')).split('=')[1];
// 2. Prevent Replay Attacks (Check if older than 5 minutes)
const currentTimestamp = Math.floor(Date.now() / 1000);
if (currentTimestamp - parseInt(timestamp, 10) > 300) {
throw new Error("Webhook timestamp is too old (replay attack risk)");
}
// 3. Compute the expected signature
// WARNING: Use the exact raw string JSON of the body, NOT the parsed object!
const signedPayload = `${timestamp}.${rawBodyString}`;
const expectedSignature = crypto
.createHmac('sha256', process.env.TALLYDECK_WEBHOOK_SECRET)
.update(signedPayload)
.digest('hex');
// 4. Compare signatures safely to prevent timing attacks
const expectedBuffer = Buffer.from(expectedSignature, 'hex');
const receivedBuffer = Buffer.from(receivedSignature, 'hex');
if (
expectedBuffer.length !== receivedBuffer.length ||
!crypto.timingSafeEqual(expectedBuffer, receivedBuffer)
) {
throw new Error("Signature Verification Failed!");
}
console.log("â
Webhook securely authenticated!");Request Body Envelope
Every webhook utilizes a standardized JSON envelope containing the event metadata, the contextual widget state, and an inner data object for event-specific payload parameters:
{
"id": "evt_abc123def4567890...",
"type": "widget.started",
"created": 1734567890,
"widget": {
"id": "e98e4f5a-c454-4632...",
"name": "Production Timer",
"type": "countdown",
"duration": 3600,
"remaining_time": 3598,
"start_time": "2026-03-28T19:00:00.000Z",
"is_running": true,
"config": {
// The full configuration object of the widget
// at the exact time of the event.
}
},
"data": {
// Event-specific payload fields (see tables below)
"action": "start"
}
}Payload Structures by Widget & Event
The internal fields of the data property vary depending on the widget type and the specific action that occurred.
đ Global Events (All Widgets)
| Event Type | Description | Payload data |
|---|---|---|
| widget.updated | Sent when core configuration definitions are modified. | { "configKeys": ["duration"] } |
âąī¸ Timers (Countdown, Pomodoro, Stopwatch, Agenda)
| Event Type | Description | Payload data |
|---|---|---|
| widget.started | Sent when the timer natively starts or resumes. | { "action": "start" } |
| widget.paused | Sent when the timer is paused. | { "action": "pause" } |
| widget.reset | Sent when the timer resets its duration/position. | { "action": "reset" } |
| timer.finished | Sent when a countdown duration naturally reaches 0. | {} |
| pomodoro.phase_changed | PomodoroSent when shifting between focus or breaks. | {} |
| agenda.item_changed | AgendaSent specifically when the active index skips/jumps. | { "index": 2 } |
đ Scoreboard
| Event Type | Description | Payload data |
|---|---|---|
| scoreboard.updated | Sent when points are modified for a specific team. | { "teamId": "uuid-str", "action": "increment", "delta": 1 } |
đĸ Counter
| Event Type | Description | Payload data |
|---|---|---|
| counter.incremented | Sent when the counter increases. | { "action": "increment" } |
| counter.decremented | Sent when the counter decreases. | { "action": "decrement" } |
| counter.reset | Sent when the counter is reset. | { "action": "reset" } |
đ Notes
| Event Type | Description | Payload data |
|---|---|---|
| note.sent | Sent when a message is dispatched to attached screens. | { "message": "The content" } |
| note.cleared | Sent when the current display message is wiped. | {} |
âī¸ Todo List
| Event Type | Description | Payload data |
|---|---|---|
| todo.updated | Dispatched as a holistic update event for list mutations. | { "action": "add" } |
đē Prompter
| Event Type | Description | Payload data |
|---|---|---|
| prompter.script_selected | Sent when a script becomes active. | { "script_id": "uuid-str" } |
| prompter.scroll_started | Sent when scrolling activates. | {} |
| prompter.scroll_paused | Sent when scrolling halts. | {} |
| prompter.scroll_reset | Sent when the position is reset to the top. | {} |