Webhooks
Receive your sessions' events in your backend: every delivery is HMAC-SHA256 signed, retried with exponential backoff, and carries a unique id so your receiver can be idempotent.
When to use webhooks
To react to what happens in the room without polling: the platform calls you when a session starts or ends, a question closes, or its results are finalized. Delivery is at-least-once — it can repeat, it never fails silently — which is why your receiver must be idempotent.
Event catalogue
Served by the API at /public/v1/limits — this page keeps no copy.
- presentation.import.completed
- presentation.import.failed
- question.activated
- question.closed
- question.results.finalized
- report.ready
- session.ended
- session.paused
- session.resumed
- session.started
Headers on every delivery
X-Votinova-EventThe event type, exactly as it appears in the catalogue.X-Votinova-Delivery-IdUnique per delivery — your deduplication key.X-Votinova-TimestampEpoch seconds; part of what is signed, so a replayed delivery cannot be re-dated.X-Votinova-SignatureHMAC-SHA256 in hex, unprefixed.
The body is compact JSON with the event and every id involved — this is, byte for byte, the signed example delivery:
{"event":"session.ended","session_id":"68b2e3d4c5f607182930a5bc"}Verifying the signature
Every delivery is signed with HMAC-SHA256 over the timestamp, a dot, and the body, keyed by the endpoint's secret. Compare the result with the X-Votinova-Signature header.
- Sign the RAW body. Parsing and re-serialising it changes the whitespace and the signature stops matching.
- Compare in constant time, not with ==.
const crypto = require("node:crypto");
function verify(headers, rawBody, secret) {
const timestamp = headers["x-votinova-timestamp"];
const signature = headers["x-votinova-signature"];
const expected = crypto
.createHmac("sha256", secret)
.update(`${timestamp}.${rawBody}`)
.digest("hex");
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
}secret whsec_3f8a1c92d4e5b6a7
timestamp 1785321600
body {"event":"session.ended","session_id":"68b2e3d4c5f607182930a5bc"}
HMAC-SHA256(secret, timestamp + "." + body)
= 10a86171bd92f5fcf8c28b873978e0214302cc2291c54cc016d18347f3ee7c38Retries
Webhook deliveries: up to 8 attempts, with increasing waits of 20, 41, 80, 185, 333, 669, 1413 seconds. Once exhausted, the delivery is dead-lettered and can be redelivered by hand.
Idempotency
Use X-Votinova-Delivery-Id as your deduplication key: a delivery can arrive more than once and your effect must apply only once.
X-Votinova-Delivery-Id
Best practices
- Answer 200 as soon as the delivery is safe and process on your own queue — a slow receiver piles up retries.
- ALWAYS verify the signature before parsing: without it, anyone who knows your URL can invent events.
- Deduplicate by X-Votinova-Delivery-Id: at-least-once means the same delivery may call twice.
- After 10 consecutive failures the endpoint pauses itself; re-enable it from the panel or over the API once your receiver is back.