Webhooks

Sonar POSTs HMAC-signed JSON to your endpoint when subscribed events fire. Subscribe in Settings / Webhooks.

Event catalog

EventWhen it fires
lead.createdA new lead row is created (UI or API).
lead.updatedA lead's status or assignment changes.
run.completedAn agent run reaches AWAITING_APPROVAL with an email draft ready.
email.approvedAn email draft is marked approved (before send).
email.sentResend has accepted the email for delivery.
email.delivered / email.bouncedResend 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

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 (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

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.