June 19, 2026 · 10 min read

Levando resultados de enquetes ao vivo para o nosso dashboard interno com webhooks

O guia de um desenvolvedor para consumir webhooks do PollsLive: verificar a assinatura HMAC, reagir a eventos vote.created e de sessão em tempo real, e recorrer ao endpoint de resultados como fallback.

By Tom Becker · Full-stack developer·DevelopersHow-toTeams & meetings

Mantemos um dashboard fixado na parede do escritório que mostra o que está acontecendo pelo produto. Eu queria resultados de enquete ao vivo nele durante o all-hands da empresa - sem martelar a API num timer. Os webhooks do PollsLive transformaram isso em algumas horas de trabalho. Aqui vai a integração, verificação de assinatura incluída, porque você nunca deve confiar num webhook não verificado.

Tip

Você não precisa ser técnico para seguir este guia. Cada passo usa botões simples no PollsLive - sem código, sem instalar app para seu público.

Os eventos que você pode assinar

Você registra uma URL de endpoint e um conjunto de eventos em Studio → Developers. Os que me importam para um dashboard ao vivo:

  • `session.started` / `session.ended` - mostrar ou desmontar o card ao vivo.
  • `response.created` - um participante respondeu a um slide numa sessão ao vivo (o de alta frequência).
  • `vote.created` - um novo voto numa enquete assíncrona.
  • `poll.published` / `poll.closed` - ciclo de vida, útil para arquivar.

Toda entrega é um `POST` JSON com o mesmo envelope: um nome de `event`, um timestamp `createdAt` e um objeto `data`.

Example delivery - response.created
POST /webhooks/pollslive  (from PollsLive → your server)
X-PollsLive-Signature: t=1750320000,v1=4f9a…c2

{
  "event": "response.created",
  "createdAt": "2026-06-19T16:00:00.000Z",
  "data": {
    "pollId": "poll_9aZ2kP",
    "sessionId": "ses_4dF1",
    "slideId": "fix",
    "kind": "multiple_choice"
  }
}

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 → Developers

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

Não coloco as *contagens* de votos no payload do webhook de propósito - o evento é um empurrãozinho que diz "algo mudou", e meu worker faz debounce de uma rajada de eventos `response.created` (um por resposta pode ser muito durante um all-hands) num único refresh que chama `GET /polls/{id}/results`. Isso me mantém bem abaixo do limite de 120 requisições/min mesmo com uma enquete movimentada.

worker.mjs - debounce + fetch results
const API = process.env.POLLSLIVE_API;
const auth = { Authorization: `Bearer ${process.env.POLLSLIVE_KEY}` };
const pending = new Map(); // pollId → timer

function onEvent(evt) {
  if (evt.event === "session.ended") return teardownTile(evt.data.pollId);
  const id = evt.data.pollId;
  clearTimeout(pending.get(id));
  pending.set(id, setTimeout(() => refresh(id), 750)); // debounce bursts
}

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.voteCounts); // websocket → the wall display
}

A parede acaba mostrando o card ao vivo abaixo - alimentado por empurrõezinhos de webhook, não por um loop de polling. No último all-hands ele atualizava em menos de um segundo a cada voto e nunca estourou o rate limit:

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 tudo7% · 13
O card do dashboard do nosso escritório, atualizado a partir de GET /polls/{id}/results a cada empurrãozinho de webhook.

Sempre tenha um fallback

Webhooks podem atrasar ou ser perdidos - redes são redes. Então a mesma função `refresh()` roda num intervalo lento de segurança (a cada 30s) enquanto a sessão está ao vivo. Cinto e suspensório: webhooks para atualizações instantâneas, um polling gentil para que o card nunca fique errado por muito tempo. Se você só fizer 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ê tem um dashboard em tempo real que também é 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 incluem o snapshot completo - armazene, não consulte a API em timer.

Results panel with stat tiles, breakdown by question, and export buttons.
The Results tab shows every answer and export options.
Session recap with final results, share buttons, and export options.
After a live session, recap shows final results and export options.
The PollsLive integrations page with PowerPoint, Google Slides, Keynote, Zoom, Microsoft Teams, Webex, and Google Meet companions.
PollsLive runs alongside your slides and meeting tools - your audience joins by QR or PIN.

Leia resultados no Studio com leia e exporte resultados de enquetes. Veja /integrations.

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