Webhooks

When you ask with async: true, we POST the result to your registered endpoint once the fan-out completes. Deliveries are signed with HMAC-SHA256 and retried with exponential backoff.

Supported events

FieldTypeDescription
ask.completedoptional
eventFires when an async or standard-mode /v1/ask finishes. The data payload contains the same NormalizedResponse you would have received from a live /v1/ask call.

Additional events (monitor runs, usage thresholds) are on the roadmap. Only ask.completed is emitted today.

Registering an endpoint

Create and manage webhook endpoints via the /v1/webhooks CRUD API or the dashboard. The signing secret is returned once at creation — store it immediately.

Payload shape

json
{
  "id": "01H8X2Y3-...",
  "type": "ask.completed",
  "created": 1714117284,
  "data": {
    /* Full /v1/ask NormalizedResponse:
       request_id, id, cached, cache_tier, providers[],
       brand_mentions[], citations[], usage { ... } */
  }
}

id identifies the event and is stable across retries; created is unix seconds at delivery time. (watch.fired events are the exception: they post their payload at the top level with no envelope — see /docs/api/watch.)

Signing

Each delivery carries an HMAC-SHA256 of the raw body keyed with your webhook secret. Verify before trusting the payload. The primary header, x-mentionsapi-signature, is the plain hex HMAC of the raw body. A legacy timestamped variant rides alongside as x-mentions-signature (t=<unix-seconds>,v1=<hex> where v1 signs <t>.<raw body>) for receivers that want replay protection baked into the MAC.

http
x-mentionsapi-signature: 5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
x-mentionsapi-timestamp: 1714117284
x-mentionsapi-event: ask.completed
x-mentionsapi-delivery-id: 7b8e6a3c-...
x-mentions-signature: t=1714117284,v1=8d1f0c2b9e...
Content-Type: application/json
verify.mjs
import crypto from "node:crypto";

export function verifyWebhook(rawBody, signatureHeader, secret) {
  // x-mentionsapi-signature: plain HMAC-SHA256 hex over the raw body.
  const expected = crypto.createHmac("sha256", secret)
    .update(rawBody)
    .digest("hex");
  return crypto.timingSafeEqual(
    Buffer.from(signatureHeader, "hex"),
    Buffer.from(expected, "hex"),
  );
}

// Optional replay protection via the legacy timestamped header
// (x-mentions-signature: "t=<sec>,v1=<hmac of '<t>.<body>'>").
export function verifyLegacyWebhook(rawBody, legacyHeader, secret) {
  const parts = Object.fromEntries(
    legacyHeader.split(",").map((p) => p.split("=")),
  );
  const expected = crypto.createHmac("sha256", secret)
    .update(`${parts.t}.${rawBody}`)
    .digest("hex");
  const ok = crypto.timingSafeEqual(
    Buffer.from(parts.v1, "hex"),
    Buffer.from(expected, "hex"),
  );
  const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
  return ok && fresh;
}

Retries

Any non-2xx response (or no response within 10s) is retried with backoff — 1min, then 5min, 3 total attempts. Every attempt is recorded; after the final failure the delivery is marked failed.

Idempotency

The payload id field is stable across retries. Store it on your side and early-return if you've already processed it — that is enough to dedupe cleanly. (The x-mentionsapi-delivery-id header is unique per delivery attempt, so use the payload id — not the header — for dedupe.)

Rotating the secret

Rotate via PATCH /v1/webhooks/:id with { "rotate_secret": true }. The response includes the new plaintext secret ONCE; swap it in your verifier, then you're done.