June 19, 2026 · 10 min read
Live-Dashboard mit PollsLive-Webhooks
Leitfaden zu PollsLive-Webhooks: HMAC-Signatur prüfen, live auf vote.created und Session-Events reagieren und auf den Ergebnis-Endpoint zurückfallen.
Wir betreiben in unserem Büro ein an die Wand montiertes Dashboard, das zeigt, was über das Produkt hinweg passiert. Ich wollte während unserer Firmen-All-Hands Live-Umfrageergebnisse darauf haben, ohne dass jemand es von Hand starten oder stoppen muss. PollsLive-Webhooks melden ihm, wann eine Session beginnt und endet, und der Results-Endpunkt liefert die Zahlen. Hier ist die Integration, inklusive Signaturprüfung, denn man sollte einem unverifizierten Webhook niemals trauen.
Die Events, die du abonnieren kannst
Du registrierst eine Endpunkt-URL und eine Reihe von Events in Studio → Entwickler-API. Die, die mir für ein Live-Dashboard wichtig sind:
- `session.started` / `session.ended`: die Live-Kachel ein- oder ausblenden.
- `response.created`: eine asynchrone Antwort auf eine Slide (das hochfrequente). Es nennt Umfrage und Slide, enthält aber keine Summen.
- `vote.created`: eine neue Stimme bei einer asynchronen Umfrage. Es enthält `totalVotes` und `voteCounts`.
- `poll.published` / `poll.closed`: Lebenszyklus, praktisch fürs Archivieren.
Eines prägt den ganzen Aufbau: Antworten in einer Live-Session senden kein Event. `response.created` und `vote.created` werden nur bei asynchronen Umfragen ausgelöst, die man über einen Link beantwortet, wann es passt. Eine Live-Session sendet `session.started` und `session.ended` und nichts dazwischen, also starten und stoppen diese beiden Events die Kachel, und die Zahlen kommen vom Results-Endpunkt.
Jede Zustellung ist ein JSON-`POST` mit derselben Hülle: einem `event`-Namen, einem `createdAt`-Zeitstempel und einem `data`-Objekt. Was in `data` steht, hängt vom Event ab. `vote.created` und `response.created` haben `pollId` auf oberster Ebene, Session-Events haben es unter `data.session.pollId`, und Poll-Events senden die Umfrage als `data.poll`. Eine `response.created`-Zustellung enthält `pollId`, `slug`, `slideId`, `kind` und bei Quizantworten `isCorrect`.
POST /webhooks/pollslive (from PollsLive → your server)
X-PollsLive-Signature: t=1750320000,v1=4f9a…c2
{
"event": "vote.created",
"createdAt": "2026-06-19T16:00:00.000Z",
"data": {
"pollId": "poll_9aZ2kP",
"slug": "hack-week",
"optionIds": ["ai"],
"totalVotes": 193,
"voteCounts": { "ai": 79, "debt": 64, "tools": 37, "wild": 13 }
}
}Verifiziere die Signatur zuerst, immer
Jede Anfrage trägt `X-PollsLive-Signature: t=<unix>,v1=<hex>`. Der `v1`-Wert ist ein HMAC-SHA256 von `<t>.<rawBody>` mit dem Signing-Secret deines Endpunkts. Du musst ihn über den rohen Request-Body berechnen (nicht das geparste JSON), in konstanter Zeit vergleichen und alles ablehnen, bei dem der Zeitstempel zu alt ist, um Replays zu blockieren.
import crypto from "node:crypto";
const SECRET = process.env.POLLSLIVE_WEBHOOK_SECRET; // from Studio → Entwickler-API
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);
}Der eine Stolperstein in Express: Du brauchst die rohen Bytes, also registriere den Verify-Handler mit `express.raw()` (oder fange den Buffer ab) statt mit `express.json()` für diese Route. Dann bestätige schnell: Gib sofort `2xx` zurück und erledige die eigentliche Arbeit asynchron, sonst behandelt PollsLive eine langsame Antwort als fehlgeschlagene Zustellung.
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);Aus Events eine Live-Kachel machen
Der Worker behandelt die beiden Fälle getrennt. Bei einer Live-Session startet `session.started` alle fünf Sekunden eine Aktualisierung, und `session.ended` beendet sie. Bei einer asynchronen Umfrage ist jedes `vote.created` oder `response.created` ein Anstoß, der sagt "etwas hat sich geändert", und der Worker entprellt einen Schwall davon (eines pro Antwort kann eine Menge sein) zu einer einzigen Aktualisierung. `vote.created` enthält zwar Zahlen, die anderen Events aber nicht, also endet jeder Weg im selben Aufruf von `GET /polls/{id}/results`. So bleibt die Zahl der Anfragen selbst bei einer belebten Umfrage niedrig; die aktuellen Werte stehen auf der Seite zu den Rate-Limits.
const API = process.env.POLLSLIVE_API;
const auth = { Authorization: `Bearer ${process.env.POLLSLIVE_KEY}` };
const pending = new Map(); // pollId → debounce timer
const loops = new Map(); // pollId → refresh loop for a live session
function onEvent(evt) {
switch (evt.event) {
case "session.started":
return startLoop(evt.data.session.pollId);
case "session.ended":
return stopLoop(evt.data.session.pollId);
case "vote.created":
case "response.created":
return nudge(evt.data.pollId);
}
}
function startLoop(pollId) {
if (loops.has(pollId)) return; // a retried delivery can arrive twice
refresh(pollId);
loops.set(pollId, setInterval(() => refresh(pollId), 5000));
}
function stopLoop(pollId) {
clearInterval(loops.get(pollId));
loops.delete(pollId);
teardownTile(pollId);
}
function nudge(pollId) {
if (pending.has(pollId)) return; // a refresh is already due: debounce bursts
pending.set(pollId, setTimeout(() => {
pending.delete(pollId);
refresh(pollId);
}, 750));
}
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.slides); // websocket → the wall display
}Der Schlüssel braucht den Scope `results:read`. Die Kachel liest `slides` aus den Ergebnissen, nicht das `voteCounts` auf oberster Ebene: Jeder Eintrag in `slides` zählt Live-Antworten und asynchrone Stimmen zusammen, während `voteCounts` nur asynchrone Stimmen zählt und bei einer Umfrage, die nur live lief, leer bleibt. Wurde eine Umfrage mehrmals präsentiert, summiert `slides` alle ihre Sessions.
An der Wand erscheint am Ende eine Kachel wie die unten. Während einer Live-Session aktualisiert sie sich alle fünf Sekunden, was für ein Wanddisplay nah genug am Raum ist:
Wohin sollen wir die Hack-Week dieses Jahr legen?
Hab immer einen Fallback
Webhooks können sich verzögern oder verloren gehen. Netzwerke sind nun mal Netzwerke. Deshalb läuft dieselbe `refresh()`-Funktion zusätzlich in einem langsamen Sicherheitsintervall (alle 30 s) für jede Umfrage an der Wand. Gürtel und Hosenträger: Webhooks für schnelle Updates, eine sanfte Abfrage, damit die Kachel nie lange falsch ist. Wenn du nur eines machst, dann die Verifizierung; wenn du zwei machst, ergänze den Fallback.
Behandle den Webhook als "Schau mal nach"-Signal, nicht als Quelle der Wahrheit. Verifiziere ihn, entprelle ihn, lies dann den Results-Endpunkt, und du bekommst ein Dashboard, das aktuell und robust ist.
Die vollständige Liste der Events und das Signaturformat stehen im Entwicklerleitfaden und der interaktiven API-Referenz. Wenn du auch die *Erstellungs*-Seite automatisierst, ist der Bericht meines Kollegen über Retros aus der API das passende Begleitstück.
Tip
Webhook-Payloads sind Anstöße, kein Ergebnis-Feed. Die meisten enthalten keine Zahlen: Fasse sie per Debounce zusammen und lies die Zahlen vom Ergebnis-Endpoint.
Lies Ergebnisse im Studio mit Umfrageergebnisse lesen und exportieren. Siehe den Integrations-Leitfaden für Webhooks und API-Endpunkte.
Entwickle auf PollsLive: Erstelle Umfragen, führe Live-Sessions durch, rufe Ergebnisse ab und empfange Webhooks aus deinem eigenen Code.
Lies die Entwicklerdokumentation