June 19, 2026 · 10 min read
Panel de resultados en vivo con webhooks de PollsLive
Guía de los webhooks de PollsLive: verifica la firma HMAC, reacciona en vivo a vote.created y a eventos de sesión, y usa el endpoint de resultados.
Tenemos un dashboard montado en la pared de la oficina que muestra lo que pasa en todo el producto. Quería resultados de encuestas en vivo en él durante el all-hands de la empresa, sin que nadie tuviera que arrancarlo o pararlo a mano. Los webhooks de PollsLive le avisan de cuándo empieza y termina una sesión, y el endpoint de resultados aporta las cifras. Aquí va la integración, con verificación de firma incluida, porque nunca deberías confiar en un webhook sin verificar.
Los eventos a los que te puedes suscribir
Registras una URL de endpoint y un conjunto de eventos en Studio → API para desarrolladores. Los que me importan para un dashboard en vivo:
- `session.started` / `session.ended`: muestra o desmonta el panel en vivo.
- `response.created`: una respuesta asíncrona a una diapositiva (el de alta frecuencia). Indica la encuesta y la diapositiva, pero no trae totales.
- `vote.created`: un nuevo voto en una encuesta asíncrona. Incluye `totalVotes` y `voteCounts`.
- `poll.published` / `poll.closed`: ciclo de vida, útil para archivar.
Hay un detalle que condiciona todo el diseño: las respuestas dadas en una sesión en vivo no envían ningún evento. `response.created` y `vote.created` solo se disparan en encuestas asíncronas, las que la gente responde desde un enlace cuando quiere. Una sesión en vivo envía `session.started` y `session.ended` y nada entre medias, así que esos dos eventos arrancan y paran el panel, y las cifras salen del endpoint de resultados.
Cada entrega es un `POST` JSON con el mismo sobre: un nombre de `event`, una marca de tiempo `createdAt` y un objeto `data`. El contenido de `data` depende del evento. `vote.created` y `response.created` llevan `pollId` en el nivel superior, los eventos de sesión lo llevan en `data.session.pollId` y los eventos de encuesta envían la encuesta como `data.poll`. Una entrega de `response.created` trae `pollId`, `slug`, `slideId`, `kind` e `isCorrect` en las respuestas de cuestionario.
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 }
}
}Verifica la firma primero, siempre
Cada petición lleva `X-PollsLive-Signature: t=<unix>,v1=<hex>`. El valor `v1` es un HMAC-SHA256 de `<t>.<rawBody>` usando el secreto de firma de tu endpoint. Debes calcularlo sobre el cuerpo crudo de la petición (no el JSON parseado), compararlo en tiempo constante, y rechazar todo aquello cuya marca de tiempo sea demasiado antigua para bloquear repeticiones.
import crypto from "node:crypto";
const SECRET = process.env.POLLSLIVE_WEBHOOK_SECRET; // from Studio → API para desarrolladores
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);
}El único detalle en Express: necesitas los bytes crudos, así que registra el handler de verificación con `express.raw()` (o captura el buffer) en lugar de `express.json()` para esa ruta. Luego confirma rápido: devuelve `2xx` de inmediato y haz el trabajo real de forma asíncrona, o PollsLive tratará una respuesta lenta como una entrega fallida.
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);Convertir eventos en un panel en vivo
El worker trata los dos casos por separado. En una sesión en vivo, `session.started` inicia un refresco cada cinco segundos y `session.ended` lo detiene. En una encuesta asíncrona, cada `vote.created` o `response.created` es un empujón que dice "algo cambió", y el worker amortigua una ráfaga de ellos (uno por respuesta puede ser mucho) en un solo refresco. `vote.created` sí trae recuentos, pero los demás eventos no, así que todos los caminos terminan en la misma llamada a `GET /polls/{id}/results`. Así el volumen de peticiones se mantiene bajo incluso con una encuesta concurrida; la página de límites de uso tiene las cifras actuales.
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 clave necesita el scope `results:read`. El panel lee `slides` de los resultados, no el `voteCounts` del nivel superior: cada entrada de `slides` cuenta juntas las respuestas en vivo y los votos asíncronos, mientras que `voteCounts` solo cuenta los votos asíncronos y queda vacío en una encuesta que solo se ha usado en vivo. Si una encuesta se ha presentado más de una vez, `slides` suma todas sus sesiones.
La pared acaba mostrando un panel como el de abajo. Durante una sesión en vivo se refresca cada cinco segundos, lo bastante cerca de la sala para una pantalla de pared:
¿Dónde deberíamos poner la hack week de este año?
Ten siempre un respaldo
Los webhooks pueden retrasarse o perderse: las redes son las redes. Así que la misma función `refresh()` también corre con un intervalo de red de seguridad lento (cada 30s) para cada encuesta de la pared. Con tirantes y cinturón: webhooks para actualizaciones rápidas, un sondeo suave para que el panel nunca esté mal por mucho tiempo. Si solo haces una cosa, haz la verificación; si haces dos, añade el respaldo.
Trata el webhook como una señal de 've a mirar', no como la fuente de la verdad. Verifícalo, amortígualo y luego lee el endpoint de resultados, y obtienes un dashboard al día y resistente.
La lista completa de eventos y el formato de la firma están en la guía para desarrolladores y en la referencia de la API interactiva. Si también estás automatizando la parte de *creación*, el artículo de mi colega sobre ejecutar retros desde la API es la pieza complementaria.
Tip
Los payloads de webhook son avisos, no un canal de resultados. La mayoría no trae recuentos: amortígualos y lee el endpoint de resultados para ver las cifras.
Lee los resultados en Studio con leer y exportar resultados de encuestas. Consulta la guía de integraciones para webhooks y endpoints de la API.
Construye sobre PollsLive: crea encuestas, dirige sesiones en vivo, descarga resultados y recibe webhooks desde tu propio código.
Lee la documentación para desarrolladores