June 19, 2026 · 10 min read
PollsLive webhooks के साथ live results dashboard
PollsLive webhooks की developer guide: HMAC signature जाँचिए, vote.created और session events पर live प्रतिक्रिया दीजिए, और results endpoint का सहारा लीजिए।
हमारे ऑफ़िस में दीवार पर लगा एक dashboard है जो दिखाता है कि पूरे product में क्या हो रहा है। मैं company all-hands के दौरान उस पर live poll results चाहता था, बिना किसी के हाथ से उसे शुरू या बंद किए। PollsLive webhooks उसे बताते हैं कि session कब शुरू हुआ और कब खत्म, और संख्याएँ results endpoint से आती हैं। यहाँ पूरा integration है, signature verification समेत, क्योंकि एक unverified webhook पर कभी भरोसा नहीं करना चाहिए।
जिन events को आप subscribe कर सकते हैं
आप Studio → डेवलपर API में एक endpoint URL और events का एक सेट register करते हैं। एक live dashboard के लिए जिन की मुझे परवाह है:
- `session.started` / `session.ended`: live tile दिखाएँ या हटाएँ।
- `response.created`: किसी slide पर एक async जवाब (high-frequency वाला)। इसमें poll और slide का नाम होता है, कुल संख्या नहीं।
- `vote.created`: किसी async poll पर एक नया vote। इसमें `totalVotes` और `voteCounts` शामिल हैं।
- `poll.published` / `poll.closed`: lifecycle, archiving के लिए काम का।
एक बात पूरे design को तय करती है: live session में दिए गए जवाबों पर कोई event नहीं आता। `response.created` और `vote.created` सिर्फ़ async polls पर चलते हैं, जिनका जवाब लोग link से अपने समय पर देते हैं। live session सिर्फ़ `session.started` और `session.ended` भेजता है, बीच में कुछ नहीं, इसलिए ये दो events tile को शुरू और बंद करते हैं, और संख्याएँ results endpoint से आती हैं।
हर delivery एक JSON `POST` है जिसका envelope एक जैसा होता है: एक `event` नाम, एक `createdAt` timestamp, और एक `data` object। `data` में क्या है, यह event पर निर्भर करता है। `vote.created` और `response.created` में `pollId` सबसे ऊपरी स्तर पर होता है, session events में `data.session.pollId` पर, और poll events poll को `data.poll` में भेजते हैं। `response.created` delivery में `pollId`, `slug`, `slideId`, `kind` होते हैं, और quiz जवाबों में `isCorrect` भी।
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 }
}
}पहले signature verify करें, हमेशा
हर request `X-PollsLive-Signature: t=<unix>,v1=<hex>` ले जाती है। `v1` value आपके endpoint के signing secret का इस्तेमाल करते हुए `<t>.<rawBody>` का एक HMAC-SHA256 है। आपको इसे raw request body पर compute करना चाहिए (parsed JSON पर नहीं), constant time में compare करना चाहिए, और जहाँ timestamp replays रोकने के लिए बहुत पुराना हो उसे reject करना चाहिए।
import crypto from "node:crypto";
const SECRET = process.env.POLLSLIVE_WEBHOOK_SECRET; // from Studio → डेवलपर API
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);
}Express में एक ही पेच: आपको raw bytes चाहिए, इसलिए उस route के लिए verify-handler को `express.json()` के बजाय `express.raw()` के साथ register करें (या buffer कैप्चर करें)। फिर जल्दी acknowledge करें: तुरंत `2xx` लौटाएँ और असली काम asynchronously करें, वरना PollsLive एक धीमे response को failed delivery मान लेगा।
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);events को एक live tile में बदलना
worker दोनों मामलों को अलग-अलग संभालता है। live session में `session.started` हर पाँच सेकंड पर refresh शुरू करता है और `session.ended` उसे रोकता है। async poll में हर `vote.created` या `response.created` एक इशारा है जो कहता है "कुछ बदला है", और worker उनकी झड़ी को (हर जवाब के लिए एक बहुत हो सकता है) एक ही refresh में debounce कर देता है। `vote.created` में counts होते हैं, पर बाकी events में नहीं, इसलिए हर रास्ता `GET /polls/{id}/results` की उसी कॉल पर खत्म होता है। इससे एक busy poll के साथ भी requests कम रहती हैं; मौजूदा सीमाएँ rate limits पेज पर हैं।
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
}key को `results:read` scope चाहिए। tile results से `slides` पढ़ता है, ऊपरी स्तर का `voteCounts` नहीं: `slides` की हर entry live जवाबों और async votes को साथ गिनती है, जबकि `voteCounts` सिर्फ़ async votes गिनता है और जो poll सिर्फ़ live चला हो उसके लिए खाली रहता है। अगर कोई poll एक से ज़्यादा बार present हुआ है, तो `slides` उसके सभी sessions को जोड़ता है।
दीवार पर आख़िरकार नीचे जैसा एक tile दिखता है। live session के दौरान यह हर पाँच सेकंड पर refresh होता है, जो दीवार की स्क्रीन के लिए काफ़ी है:
इस साल का hack week हमें कहाँ रखना चाहिए?
हमेशा एक fallback रखें
webhooks में देरी हो सकती है या वे ड्रॉप हो सकते हैं: नेटवर्क आख़िर नेटवर्क हैं। तो वही `refresh()` function दीवार पर दिख रहे हर poll के लिए एक धीमे सेफ़्टी-नेट अंतराल (हर 30s) पर भी चलता है। पूरी ऐहतियात: जल्दी अपडेट के लिए webhooks, और एक हल्का poll ताकि tile कभी ज़्यादा देर ग़लत न रहे। अगर सिर्फ़ एक चीज़ करें, तो verification करें; अगर दो करें, तो fallback जोड़ें।
webhook को 'जाकर देखो' संकेत मानें, सच का स्रोत नहीं। इसे verify करें, debounce करें, फिर results endpoint पढ़ें, और आपको एक dashboard मिलता है जो अप-टू-डेट और टिकाऊ है।
events की पूरी सूची और signature फ़ॉर्मैट developer guide और इंटरैक्टिव API reference में हैं। अगर आप *बनाने* वाला हिस्सा भी ऑटोमेट कर रहे हैं, तो मेरे साथी का API से retros चलाने वाला लेख साथी टुकड़ा है।
Tip
webhook payloads संकेत हैं, results feed नहीं। ज़्यादातर में counts नहीं होते, इसलिए उन्हें debounce करें और आँकड़ों के लिए results endpoint पढ़ें।
Studio mein results poll results padhna aur export se dekhein. Dekhein integrations guide.
PollsLive पर बनाएँ: अपने ही कोड से polls बनाएँ, live sessions चलाएँ, results खींचें, और webhooks पाएँ।
developer docs पढ़ें