Webhooks

Add an endpoint under Developers in the portal, pick the events you want, and we POST JSON to it when they happen.

Events

  • question.reserved
  • question.completed
  • question.underfilled
  • question.cancelled
  • question.refused
  • credits.purchased
  • credits.low
  • credits.expiring
  • dispute.resolved
  • connection.cap_warning

Payload

{
  "id": "evt_3kd9",
  "type": "question.completed",
  "createdAt": "2026-09-24T10:12:00Z",
  "data": {
    "questionId": "q_8k2f",
    "status": "complete",
    "summary": {
      "winner": "Menu B",
      "margin": 36,
      "confidence": "high"
    },
    "creditsSpent": 600
  }
}

Verifying signatures

Every delivery carries x-50heads-signature: t=<unix seconds>,v1=<hex>, where v1 is the HMAC-SHA256 of <t>.<raw body> with your endpoint's secret. Check it against the raw bytes before parsing, and reject timestamps more than five minutes old.

import { createHmac, timingSafeEqual } from "node:crypto";

// Header: x-50heads-signature: t=<unix seconds>,v1=<hex HMAC-SHA256 of "<t>.<raw body>">
export function verify(rawBody: string, header: string, secret: string, toleranceSec = 300) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=") as [string, string]));
  const t = Number(parts.t);
  if (!Number.isFinite(t) || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;
  const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
  const given = Buffer.from(parts.v1 ?? "", "hex");
  const wanted = Buffer.from(expected, "hex");
  return given.length === wanted.length && timingSafeEqual(given, wanted);
}

Retries and replay

Answer 2xx within ten seconds. Anything else is retried with backoff, eight times over 24 hours. Deliveries can arrive more than once or out of order: use the event id to ignore repeats. The delivery log and a replay button are under Developers in the portal.