June 19, 2026 · 10 min read
Painel de resultados ao vivo com webhooks do PollsLive
Guia dos webhooks do PollsLive: verifique a assinatura HMAC, reaja ao vivo a vote.created e a eventos de sessão, e use o endpoint de resultados como reserva.
Temos um dashboard montado na parede do escritório que mostra o que está acontecendo em todo o produto. Eu queria resultados de enquetes ao vivo nele durante os all-hands da empresa, sem ninguém precisar iniciá-lo ou pará-lo à mão. Os webhooks do PollsLive avisam quando uma sessão começa e termina, e o endpoint de resultados fornece os números. Aqui vai a integração, com verificação de assinatura incluída, porque você nunca deve confiar num webhook não verificado.
Os eventos que você pode assinar
Você registra uma URL de endpoint e um conjunto de eventos em Studio → API para desenvolvedores. Os que me importam para um dashboard ao vivo:
- `session.started` / `session.ended`: mostrar ou desmontar o card ao vivo.
- `response.created`: uma resposta assíncrona a um slide (o de alta frequência). Ele indica a enquete e o slide, mas não traz totais.
- `vote.created`: um novo voto numa enquete assíncrona. Inclui `totalVotes` e `voteCounts`.
- `poll.published` / `poll.closed`: ciclo de vida, útil para arquivar.
Um ponto define todo o desenho: respostas dadas numa sessão ao vivo não enviam nenhum evento. `response.created` e `vote.created` só disparam em enquetes assíncronas, aquelas que as pessoas respondem por um link quando querem. Uma sessão ao vivo envia `session.started` e `session.ended` e nada no meio, então esses dois eventos iniciam e param o card, e os números vêm do endpoint de resultados.
Cada entrega é um `POST` JSON com o mesmo envelope: um nome de `event`, um timestamp `createdAt` e um objeto `data`. O conteúdo de `data` depende do evento. `vote.created` e `response.created` têm `pollId` no nível superior, os eventos de sessão têm em `data.session.pollId`, e os eventos de enquete enviam a enquete como `data.poll`. Uma entrega de `response.created` traz `pollId`, `slug`, `slideId`, `kind` e `isCorrect` nas respostas 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 }
}
}Verifique a assinatura primeiro, sempre
Cada requisição carrega `X-PollsLive-Signature: t=<unix>,v1=<hex>`. O valor `v1` é um HMAC-SHA256 de `<t>.<rawBody>` usando o segredo de assinatura do seu endpoint. Você deve computá-lo sobre o corpo bruto da requisição (não o JSON parseado), comparar em tempo constante e rejeitar qualquer coisa em que o timestamp seja velho demais, para bloquear replays.
import crypto from "node:crypto";
const SECRET = process.env.POLLSLIVE_WEBHOOK_SECRET; // from Studio → API para desenvolvedores
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);
}A única pegadinha no Express: você precisa dos bytes brutos, então registre o handler de verificação com `express.raw()` (ou capture o buffer) em vez de `express.json()` para essa rota. Depois confirme rápido: retorne `2xx` imediatamente e faça o trabalho de verdade de forma assíncrona, ou o PollsLive vai tratar uma resposta lenta como uma entrega que falhou.
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);Transformando eventos num card ao vivo
O worker trata os dois casos separadamente. Numa sessão ao vivo, `session.started` inicia um refresh a cada cinco segundos e `session.ended` o interrompe. Numa enquete assíncrona, cada `vote.created` ou `response.created` é um empurrãozinho que diz "algo mudou", e o worker faz debounce de uma rajada deles (um por resposta pode ser muito) num único refresh. `vote.created` traz contagens, mas os outros eventos não, então todo caminho termina na mesma chamada a `GET /polls/{id}/results`. Assim o volume de requisições fica baixo mesmo com uma enquete movimentada; a página de limites de uso traz os números atuais.
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
}A chave precisa do escopo `results:read`. O card lê `slides` dos resultados, não o `voteCounts` do nível superior: cada entrada de `slides` conta juntas as respostas ao vivo e os votos assíncronos, enquanto `voteCounts` conta só os votos assíncronos e fica vazio numa enquete usada apenas ao vivo. Se uma enquete foi apresentada mais de uma vez, `slides` soma todas as suas sessões.
A parede acaba mostrando um card como o abaixo. Durante uma sessão ao vivo ele atualiza a cada cinco segundos, o que fica perto o bastante da sala para uma tela de parede:
Onde devemos fazer a hack week deste ano?
Sempre tenha um fallback
Webhooks podem atrasar ou se perder. Redes são redes. Por isso a mesma função `refresh()` também roda num intervalo lento de segurança (a cada 30s) para cada enquete na parede. Cinto e suspensório: webhooks para atualizações rápidas, um polling suave para que o card nunca fique errado por muito tempo. Se você fizer só uma coisa, faça a verificação; se fizer duas, adicione o fallback.
Trate o webhook como um sinal de "vá olhar", não como a fonte da verdade. Verifique, faça debounce e então leia o endpoint de resultados, e você terá um dashboard atualizado e resiliente.
A lista completa de eventos e o formato da assinatura estão no guia para desenvolvedores e na referência de API interativa. Se você está automatizando o lado da *criação* também, o artigo do meu colega sobre rodar retros a partir da API é a peça complementar.
Tip
Payloads de webhook são avisos, não um feed de resultados. A maioria não traz contagens: agrupe-os e leia o endpoint de resultados para ver os números.
Leia resultados no Studio com leia e exporte resultados de enquetes. Veja o guia de integrações para webhooks e endpoints da API.
Desenvolva no PollsLive: crie enquetes, conduza sessões ao vivo, obtenha resultados e receba webhooks pelo seu próprio código.
Leia a documentação para desenvolvedores