Webhooks

Recibe los eventos de tus sesiones en tu backend: cada entrega viaja firmada con HMAC-SHA256, se reintenta con backoff exponencial y lleva un identificador único para que tu receptor sea idempotente.

Cuándo usar webhooks

Para reaccionar a lo que pasa en la sala sin sondear: la plataforma te llama cuando una sesión arranca o termina, una pregunta cierra o sus resultados quedan consolidados. La entrega es al-menos-una-vez — puede repetirse, nunca perderse en silencio — y por eso tu receptor debe ser idempotente.

Catálogo de eventos

Servido por la API en /public/v1/limits — esta página no mantiene una copia.

  • presentation.import.completed
  • presentation.import.failed
  • question.activated
  • question.closed
  • question.results.finalized
  • report.ready
  • session.ended
  • session.paused
  • session.resumed
  • session.started

Cabeceras de cada entrega

  • X-Votinova-EventEl tipo de evento, tal como aparece en el catálogo.
  • X-Votinova-Delivery-IdÚnica por entrega — tu clave de deduplicación.
  • X-Votinova-TimestampÉpoca en segundos; forma parte de lo firmado, así que una entrega repetida no puede re-datarse.
  • X-Votinova-SignatureHMAC-SHA256 en hexadecimal, sin prefijo.

El cuerpo es JSON compacto con el evento y todos los identificadores implicados — este es, byte a byte, el de la entrega firmada del ejemplo:

{"event":"session.ended","session_id":"68b2e3d4c5f607182930a5bc"}

Verificar la firma

Cada entrega llega firmada con HMAC-SHA256 sobre «marca de tiempo, un punto y el cuerpo», con el secreto del endpoint. Compara el resultado con la cabecera X-Votinova-Signature.

  • Firma sobre el cuerpo CRUDO. Si lo parseas y lo vuelves a serializar cambian los espacios y la firma deja de cuadrar.
  • Compara en tiempo constante, no con ==.
Verificado contra una entrega real
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)
= 10a86171bd92f5fcf8c28b873978e0214302cc2291c54cc016d18347f3ee7c38

Reintentos

Entregas de webhook: hasta 8 intentos, con esperas crecientes de 20, 47, 92, 174, 368, 748, 1457 segundos. Agotados, la entrega queda en la cola muerta y se puede reenviar a mano.

Idempotencia

Usa X-Votinova-Delivery-Id como clave de deduplicación: una entrega puede llegar más de una vez y tu efecto debe aplicarse solo una.

X-Votinova-Delivery-Id

Buenas prácticas

  • Responde 200 en cuanto la entrega esté a salvo y procesa en tu cola — un receptor lento acumula reintentos.
  • Verifica la firma SIEMPRE antes de parsear: sin ella, cualquiera que conozca tu URL puede inventarse eventos.
  • Deduplica por X-Votinova-Delivery-Id: al-menos-una-vez significa que la misma entrega puede llamar dos veces.
  • Tras 10 fallos consecutivos el endpoint se pausa solo; reactívalo desde el panel o por API cuando tu receptor vuelva.