Webhooks
Sonar POSTs HMAC-signed JSON to your endpoint when subscribed events fire. Subscribe in Settings / Webhooks.
Event catalog
| Event | When it fires |
|---|---|
lead.created | A new lead row is created (UI or API). |
lead.updated | A lead's status or assignment changes. |
run.completed | An agent run reaches AWAITING_APPROVAL with an email draft ready. |
email.approved | An email draft is marked approved (before send). |
email.sent | Resend has accepted the email for delivery. |
email.delivered / email.bounced | Resend delivery webhook reports the final state. |
Payload shape
json
{
"id": "a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
"type": "lead.created",
"orgId": "...",
"deliveredAt": "2026-05-15T14:23:00.000Z",
"data": {
"leadId": "8c5a...",
"name": "Jane Doe",
"status": "DISCOVERY"
}
}Signature verification
Every request carries:
code
X-Sonar-Signature: t=<unix>,v1=<hmac_sha256_hex>The signed string is `${timestamp}.${rawBody}`. Reject if t is older than 5 minutes (replay protection) or the HMAC mismatches.
Node.js
javascript
import { createHmac, timingSafeEqual } from "node:crypto";
function verifySonarSignature(rawBody, header, secret) {
const parts = Object.fromEntries(
header.split(",").map((p) => p.split("="))
);
const t = Number(parts.t);
if (!t || Math.abs(Math.floor(Date.now() / 1000) - t) > 300) return false;
const expected = createHmac("sha256", secret)
.update(`${t}.${rawBody}`)
.digest("hex");
const received = Buffer.from(parts.v1 ?? "", "hex");
const digest = Buffer.from(expected, "hex");
// timingSafeEqual throws on length mismatch, so guard first.
return received.length === digest.length && timingSafeEqual(received, digest);
}Python
python
import hmac, hashlib, time
def verify_sonar_signature(raw_body: bytes, header: str, secret: str) -> bool:
parts = dict(p.split("=") for p in header.split(","))
t = int(parts["t"])
if abs(int(time.time()) - t) > 300:
return False
expected = hmac.new(
secret.encode(),
f"{t}.".encode() + raw_body,
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(parts["v1"], expected)Delivery + retries
Failed deliveries (HTTP not 2xx or timeout greater than 10s) are logged with status: FAILED in the workspace UI and can be replayed manually. Automatic retry with exponential backoff ships in the next release.
Build for idempotency on your side: use
event.id as a dedupe key. Sonar may re-deliver an event if you click replay.