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
| Field | Type | Description |
|---|---|---|
ask.completedoptional | event | Fires 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
{
"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.
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/jsonimport 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.