Skip to content

Outbound webhooks

Dial Dozer sends active subscriptions an HTTP POST with JSON and an HMAC signature.

Content-Type: application/json
X-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"}
}

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.

EventData fields emitted todaySource
lead.createdlead_id, plus creation-dependent campaign, source, phone, first_name, last_nameAPI lead intake; inbound SMS creation uses only lead_id, source, phone
message.receivedlead_id, message_id, channel, from, bodyTelnyx/Twilio/SendGrid inbound storage; body is truncated to 120 characters
dispo.setlead_id, status, campaignDialer disposition
appt.bookedlead_id, appt_id, epoch, campaignDialer booking paths
appt.outcomelead_id, appt_id, event, campaignAppointment 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.

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.