Outbound webhooks
Dial Dozer sends active subscriptions an HTTP POST with JSON and an HMAC signature.
Content-Type: application/jsonX-Npk-Signature: sha256=4a9c...{ "event": "lead.created", "ts": 1784232000, "data": {"lead_id":48219,"campaign":"SOLAR","source":"website-demo","phone":"6025550142","first_name":"Maya","last_name":"Chen"}}Verify every delivery
Section titled “Verify every delivery”Compute HMAC-SHA256 over the exact raw request bytes with the subscription secret, prefix the lowercase hex digest with sha256=, then use a constant-time comparison.
import crypto from 'node:crypto';
export function validDialDozerWebhook(rawBody, signature, secret) { const expected = `sha256=${crypto.createHmac('sha256', secret).update(rawBody).digest('hex')}`; const a = Buffer.from(signature || ''); const b = Buffer.from(expected); return a.length === b.length && crypto.timingSafeEqual(a, b);}Do not parse/re-serialize before verification. ts is Unix seconds, but current sender code does not enforce receiver replay windows or send a unique delivery ID. Build idempotency from stable event data where possible (for example event + data.message_id) and reject timestamps outside your own tolerance.
Event catalog
Section titled “Event catalog”| Event | Data fields emitted today | Source |
|---|---|---|
lead.created | lead_id, plus creation-dependent campaign, source, phone, first_name, last_name | API lead intake; inbound SMS creation uses only lead_id, source, phone |
message.received | lead_id, message_id, channel, from, body | Telnyx/Twilio/SendGrid inbound storage; body is truncated to 120 characters |
dispo.set | lead_id, status, campaign | Dialer disposition |
appt.booked | lead_id, appt_id, epoch, campaign | Dialer booking paths |
appt.outcome | lead_id, appt_id, event, campaign | Appointment outcome timeline |
The admin event allowlist is exactly those five names. An empty subscription event list means all events. Values may be stored as a comma list or JSON array internally.
Delivery behavior
Section titled “Delivery behavior”Each event is delivered synchronously from the business action. Per subscription, Dial Dozer uses an 8-second timeout and retries once immediately when it does not receive 2xx. Failures are logged and swallowed so the original business action continues. There is currently no durable queue, exponential backoff, delivery history API, or manual redelivery endpoint. Keep receivers fast: validate, persist/enqueue, return 2xx.
Subscriptions with a URL that does not start with http:// or https:// are skipped. Production receivers should use HTTPS.
Current sender code reads every active subscription without filtering by organization. In a multi-tenant self-hosted install, administrators must treat webhook subscription management as platform-wide until tenant filtering is added.