Ir para o conteúdo

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.

By the PollsLive team·DevelopersHow-toTeams & meetings

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.

Example delivery (vote.created)
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.

verify.mjs
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.

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);

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.

worker.mjs (debounce + fetch results)
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:

Live poll

Onde devemos fazer a hack week deste ano?

Recursos de IA para o produto principal41% · 79
Quitar dívida técnica / confiabilidade33% · 64
Ferramentas internas que nos poupam tempo19% · 37
Curinga (vale tudo)7% · 13
Um card de exemplo, preenchido a partir de GET /polls/{id}/results.

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

Os cookies essenciais mantêm a sua sessão iniciada e evitam votos duplicados. Com a sua autorização, também utilizamos o Cloudflare Web Analytics para contar visualizações de páginas e o Whop Pixel para saber que visitas levam a uma compra. Política de cookies