June 19, 2026 · 10 min read

Piping live poll results into our internal dashboard with webhooks

A developer's guide to consuming PollsLive webhooks: verify the HMAC signature, react to vote.created and session events in real time, and fall back to polling the results endpoint.

By Tom Becker · Full-stack developer·DevelopersHow-toTeams & meetings

We run a wall-mounted dashboard in our office that shows what's happening across the product. I wanted live poll results on it during company all-hands - without hammering the API on a timer. PollsLive webhooks made it a couple of hours of work. Here's the integration, signature verification included, because you should never trust an unverified webhook.

Tip

You do not need to be technical to follow this guide. Every step uses plain buttons in PollsLive - no coding, no app install for your audience.

The events you can subscribe to

You register an endpoint URL and a set of events in Studio → Developers. The ones I care about for a live dashboard:

  • `session.started` / `session.ended` - show or tear down the live tile.
  • `response.created` - a participant answered a slide in a live session (the high-frequency one).
  • `vote.created` - a new vote on an async poll.
  • `poll.published` / `poll.closed` - lifecycle, handy for archiving.

Every delivery is a JSON `POST` with the same envelope: an `event` name, a `createdAt` timestamp, and a `data` object.

Example delivery - response.created
POST /webhooks/pollslive  (from PollsLive → your server)
X-PollsLive-Signature: t=1750320000,v1=4f9a…c2

{
  "event": "response.created",
  "createdAt": "2026-06-19T16:00:00.000Z",
  "data": {
    "pollId": "poll_9aZ2kP",
    "sessionId": "ses_4dF1",
    "slideId": "fix",
    "kind": "multiple_choice"
  }
}

Verify the signature first - always

Each request carries `X-PollsLive-Signature: t=<unix>,v1=<hex>`. The `v1` value is an HMAC-SHA256 of `<t>.<rawBody>` using your endpoint's signing secret. You must compute it over the raw request body (not the parsed JSON), compare in constant time, and reject anything where the timestamp is too old to block replays.

verify.mjs
import crypto from "node:crypto";

const SECRET = process.env.POLLSLIVE_WEBHOOK_SECRET; // from Studio → Developers

export function verifyPollsLive(rawBody, header, toleranceSec = 300) {
  // header looks like: "t=1750320000,v1=4f9a…c2"
  const parts = Object.fromEntries(
    header.split(",").map((kv) => kv.split("=")),
  );
  const t = Number(parts.t);
  if (!t || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;

  const expected = crypto
    .createHmac("sha256", SECRET)
    .update(`${t}.${rawBody}`)
    .digest("hex");

  const a = Buffer.from(expected);
  const b = Buffer.from(parts.v1 ?? "");
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

The one gotcha in Express: you need the raw bytes, so register the verify-handler with `express.raw()` (or capture the buffer) instead of `express.json()` for that route. Then acknowledge fast - return `2xx` immediately and do the real work asynchronously, or PollsLive will treat a slow response as a failed delivery.

server.mjs
import express from "express";
import { verifyPollsLive } from "./verify.mjs";

const app = express();

app.post(
  "/webhooks/pollslive",
  express.raw({ type: "application/json" }),
  (req, res) => {
    const sig = req.get("X-PollsLive-Signature") ?? "";
    if (!verifyPollsLive(req.body.toString("utf8"), sig)) {
      return res.status(400).send("bad signature");
    }
    const evt = JSON.parse(req.body.toString("utf8"));

    // ACK first, then process out of band.
    res.sendStatus(200);
    queue.add(evt); // e.g. push to a worker / pub-sub / websocket fan-out
  },
);

app.listen(3000);

Turning events into a live tile

I don't put vote *counts* in the webhook payload on purpose - the event is a nudge that says "something changed", and my worker debounces a burst of `response.created` events (one per answer can be a lot during an all-hands) into a single refresh that calls `GET /polls/{id}/results`. That keeps me well under the 120 requests/min limit even with a busy poll.

worker.mjs - debounce + fetch results
const API = process.env.POLLSLIVE_API;
const auth = { Authorization: `Bearer ${process.env.POLLSLIVE_KEY}` };
const pending = new Map(); // pollId → timer

function onEvent(evt) {
  if (evt.event === "session.ended") return teardownTile(evt.data.pollId);
  const id = evt.data.pollId;
  clearTimeout(pending.get(id));
  pending.set(id, setTimeout(() => refresh(id), 750)); // debounce bursts
}

async function refresh(pollId) {
  const res = await fetch(`${API}/polls/${pollId}/results`, { headers: auth });
  if (res.status === 429) {                 // backed-off retry on rate limit
    return setTimeout(() => refresh(pollId), 2000);
  }
  const { data } = await res.json();
  pushToDashboard(pollId, data.voteCounts); // websocket → the wall display
}

The wall ends up showing the live tile below - fed by webhook nudges, not a polling loop. During the last all-hands it updated within a second of each vote and never tripped the rate limit:

Live poll

Where should we put this year's hack week?

AI features for the core product41% · 79
Pay down tech debt / reliability33% · 64
Internal tools that save us time19% · 37
Wildcard - anything goes7% · 13
Our office dashboard tile, refreshed from GET /polls/{id}/results on each webhook nudge.

Always have a fallback

Webhooks can be delayed or dropped - networks are networks. So the same `refresh()` function runs on a slow safety-net interval (every 30s) while a session is live. Belt and braces: webhooks for instant updates, a gentle poll so the tile is never wrong for long. If you only do one, do verification; if you do two, add the fallback.

Treat the webhook as a 'go look' signal, not the source of truth. Verify it, debounce it, then read the results endpoint - and you get a real-time dashboard that's also resilient.

The full list of events and the signature format live in the developer guide and the interactive API reference. If you're automating the *creation* side too, my colleague's write-up on running retros from the API is the companion piece.

Tip

Webhook payloads include the full result snapshot - store them, do not poll the API on a timer.

Results panel with stat tiles, breakdown by question, and export buttons.
The Results tab shows every answer and export options.
Session recap with final results, share buttons, and export options.
After a live session, recap shows final results and export options.
The PollsLive integrations page with PowerPoint, Google Slides, Keynote, Zoom, Microsoft Teams, Webex, and Google Meet companions.
PollsLive runs alongside your slides and meeting tools - your audience joins by QR or PIN.

Read results in Studio with read and export poll results. See /integrations.

Build on PollsLive - create polls, drive live sessions, pull results, and receive webhooks from your own code.

Read the developer docs