June 19, 2026 · 10 min read
Tableau de bord en direct avec les webhooks PollsLive
Guide des webhooks PollsLive : vérifiez la signature HMAC, réagissez en direct à vote.created et aux événements de session, et repliez-vous sur l'endpoint.
Nous avons au bureau un dashboard mural qui montre ce qui se passe dans le produit. Je voulais y afficher des résultats de sondage en direct pendant les all-hands de l'entreprise, sans que personne ait à le lancer ou l'arrêter à la main. Les webhooks PollsLive lui signalent quand une session commence et se termine, et l'endpoint de résultats fournit les chiffres. Voici l'intégration, vérification de signature comprise, parce qu'il ne faut jamais faire confiance à un webhook non vérifié.
Les événements auxquels vous pouvez vous abonner
Vous enregistrez une URL d'endpoint et un ensemble d'événements dans Studio → API développeur. Ceux qui m'intéressent pour un dashboard live :
- `session.started` / `session.ended` : afficher ou démonter la tuile live.
- `response.created` : une réponse asynchrone à une diapositive (l'événement à haute fréquence). Il indique le sondage et la diapositive, mais aucun total.
- `vote.created` : un nouveau vote sur un sondage asynchrone. Il inclut `totalVotes` et `voteCounts`.
- `poll.published` / `poll.closed` : cycle de vie, pratique pour l'archivage.
Un point conditionne toute la conception : les réponses données pendant une session live n'envoient aucun événement. `response.created` et `vote.created` ne se déclenchent que pour les sondages asynchrones, ceux auxquels on répond depuis un lien quand on veut. Une session live envoie `session.started` et `session.ended`, et rien entre les deux : ces deux événements lancent et arrêtent donc la tuile, et les chiffres viennent de l'endpoint de résultats.
Chaque livraison est un `POST` JSON avec la même enveloppe : un nom d'`event`, un timestamp `createdAt`, et un objet `data`. Le contenu de `data` dépend de l'événement. `vote.created` et `response.created` ont `pollId` au premier niveau, les événements de session l'ont dans `data.session.pollId`, et les événements de sondage envoient le sondage dans `data.poll`. Une livraison `response.created` contient `pollId`, `slug`, `slideId`, `kind`, et `isCorrect` pour les réponses de quiz.
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 }
}
}Vérifiez la signature en premier, toujours
Chaque requête porte `X-PollsLive-Signature: t=<unix>,v1=<hex>`. La valeur `v1` est un HMAC-SHA256 de `<t>.<rawBody>` utilisant le secret de signature de votre endpoint. Vous devez le calculer sur le corps de requête brut (pas le JSON parsé), comparer en temps constant, et rejeter tout ce dont le timestamp est trop ancien pour bloquer les rejeux.
import crypto from "node:crypto";
const SECRET = process.env.POLLSLIVE_WEBHOOK_SECRET; // from Studio → API développeur
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);
}Le seul piège avec Express : il vous faut les octets bruts, donc enregistrez le handler de vérification avec `express.raw()` (ou capturez le buffer) au lieu d'`express.json()` pour cette route. Ensuite, accusez réception rapidement : renvoyez `2xx` immédiatement et faites le vrai travail de façon asynchrone, sinon PollsLive traitera une réponse lente comme une livraison échouée.
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);Transformer les événements en tuile live
Le worker traite les deux cas séparément. Pour une session live, `session.started` lance un rafraîchissement toutes les cinq secondes et `session.ended` l'arrête. Pour un sondage asynchrone, chaque `vote.created` ou `response.created` est un coup de coude qui dit "quelque chose a changé", et le worker regroupe une rafale de ces événements (un par réponse, ça peut faire beaucoup) en un seul rafraîchissement. `vote.created` contient bien des décomptes, mais pas les autres événements, donc chaque chemin aboutit au même appel à `GET /polls/{id}/results`. Le volume de requêtes reste faible même avec un sondage très actif ; la page des limites de débit donne les chiffres actuels.
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
}La clé a besoin du scope `results:read`. La tuile lit `slides` dans les résultats, pas le `voteCounts` de premier niveau : chaque entrée de `slides` compte ensemble les réponses live et les votes asynchrones, alors que `voteCounts` ne compte que les votes asynchrones et reste vide pour un sondage utilisé uniquement en live. Si un sondage a été présenté plusieurs fois, `slides` additionne toutes ses sessions.
Le mur finit par afficher une tuile comme celle ci-dessous. Pendant une session live, elle se rafraîchit toutes les cinq secondes, ce qui colle d'assez près à la salle pour un écran mural :
Où devrait-on placer la hack week de cette année ?
Ayez toujours un plan B
Les webhooks peuvent être retardés ou perdus : les réseaux restent des réseaux. Du coup, la même fonction `refresh()` tourne aussi sur un intervalle de filet de sécurité tranquille (toutes les 30 s) pour chaque sondage affiché au mur. Ceinture et bretelles : les webhooks pour des mises à jour rapides, un polling doux pour que la tuile ne soit jamais fausse bien longtemps. Si vous ne faites qu'une chose, faites la vérification ; si vous en faites deux, ajoutez le plan B.
Traitez le webhook comme un signal « va voir », pas comme la source de vérité. Vérifiez-le, regroupez-le, puis lisez l'endpoint de résultats, et vous obtenez un dashboard à jour et résilient.
La liste complète des événements et le format de signature vivent dans le guide développeur et la référence d'API interactive. Si vous automatisez aussi le côté *création*, l'article de ma collègue sur comment lancer des rétros depuis l'API en est le pendant.
Tip
Les payloads de webhook sont des signaux, pas un flux de résultats. La plupart ne contiennent aucun décompte : regroupez-les et lisez l'endpoint de résultats pour obtenir les chiffres.
Consultez les résultats dans Studio avec lire et exporter les résultats. Voir le guide des intégrations pour les webhooks et les endpoints API.
Construisez sur PollsLive : créez des sondages, pilotez des sessions live, récupérez des résultats et recevez des webhooks depuis votre propre code.
Lire la doc développeur