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": "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.