Webhooks
A webhook alert POSTs its digest to a URL you control, signed so you can prove it came from BrandSonar.
Webhooks are one of the three destinations an alert can send to. Everything about configuring the alert, including its filters and its schedule, works the same as it does for email and Slack; this page covers what arrives at the other end.
Setting one up
Create an alert, choose Webhook, and give it an HTTPS URL. BrandSonar rejects plain HTTP, and rejects addresses that resolve to private infrastructure such as loopback, private ranges, link-local, and bare internal hostnames, so the endpoint has to be publicly reachable.
A signing secret beginning whsec_ is generated when the alert is created, and shown in the alert's settings. Store it wherever your receiver keeps its secrets. Regenerating it from the same screen invalidates the old one immediately.
The request
- Method
POST, with aContent-Type: application/jsonbody.X-BrandSonar-Event- The event name. Currently always
alert.digest. X-BrandSonar-Timestamp- Unix seconds at which the request was signed.
X-BrandSonar-Signaturesha256=<hex>, the HMAC-SHA256 of`${timestamp}.${rawBody}`using the alert's secret.- User-Agent
BrandSonar-Webhook/1.0
Respond with any 2xx status within 10 seconds. Anything else is treated as a failed delivery and logged: a non-2xx, a timeout, or a redirect, which is not followed. Do the real work asynchronously and acknowledge fast.
The payload
{
"event": "alert.digest",
"deliveredAt": "2026-08-15T09:00:00.000Z",
"alert": {
"id": "019f21a0-4c11-7e3a-9d02-6b8e0f14c7d5",
"name": "Negative mentions"
},
"brand": {
"id": "019edc1a-6383-7c72-9f3c-e19dd5e91f6c",
"name": "Acme"
},
"totalCount": 3,
"mentions": [
{
"id": "019edc44-1f02-7a90-b3c8-27d51e6a0f4b",
"platform": "reddit",
"sentiment": "negative",
"title": null,
"content": "Acme's onboarding flow lost my import halfway through.",
"authorName": null,
"authorUsername": "jordanops",
"sourceUrl": "https://reddit.com/r/saas/comments/...",
"relevanceScore": 88,
"publishedAt": "2026-08-15T08:41:00.000Z"
}
],
"dashboardUrl": "https://brandsonar.com/mentions"
}event- Always
alert.digesttoday. Check it rather than assuming, so a future event type does not break your handler. deliveredAt- ISO 8601 timestamp of the delivery.
alert- The
idandnameof the alert that fired. brand- The
idandnameof the brand it watches. totalCount- How many mentions matched. This can exceed the length of
mentions. mentions- The matching mentions, capped at 50 per delivery. Fetch the rest through the REST API when
totalCountis larger. dashboardUrl- A link back into the app.
Verifying the signature
Recompute the HMAC over the raw request body and compare it against the header in constant time. Verify before parsing: JSON.parse followed by re-serialising produces different bytes and a signature that will never match.
import { createHmac, timingSafeEqual } from "node:crypto";
// rawBody must be the exact bytes received. Parse the JSON only after
// verifying, since re-serialising changes the signature.
export function verify(rawBody: string, headers: Headers, secret: string) {
const timestamp = headers.get("X-BrandSonar-Timestamp");
const received = headers.get("X-BrandSonar-Signature");
if (!timestamp || !received) return false;
// Reject replays of an old, valid delivery.
const age = Math.abs(Date.now() / 1000 - Number(timestamp));
if (!Number.isFinite(age) || age > 300) return false;
const hex = createHmac("sha256", secret)
.update(`${timestamp}.${rawBody}`)
.digest("hex");
const expected = Buffer.from(`sha256=${hex}`);
const actual = Buffer.from(received);
return expected.length === actual.length && timingSafeEqual(expected, actual);
}Comparing the timestamp against the current time, as above, is what stops an attacker replaying a delivery they captured earlier. Five minutes is a reasonable window.
Rotating the secret
Open the alert in Alerts and regenerate the signing secret. The old secret stops working the moment you do, so update your receiver first if it validates strictly.
A failed delivery does not advance the alert's cursor, so the same mentions are attempted again on the next run. Key your handler on each mention's id so a redelivery is a no-op rather than a duplicate.