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": "evt_8a3f...", "type": "lead.created", "orgId": "...", "deliveredAt": "2026-05-15T14:23:00.000Z", "data": { "leadId": "8c5a...", "name": "Jane Doe", "status": "DISCOVERY" } }
Signature verification
Every request carries:
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
javascriptimport { 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 (Math.abs(Math.floor(Date.now() / 1000) - t) > 300) return false; const expected = createHmac("sha256", secret) .update(`${t}.${rawBody}`) .digest("hex"); return timingSafeEqual( Buffer.from(parts.v1, "hex"), Buffer.from(expected, "hex") ); }
Python
pythonimport 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.