Webhooks

View as Markdown

A webhook subscription delivers your account’s events to an HTTPS endpoint you control. Register a URL and the event types you care about, and Dial POSTs each matching event to that URL — signed, retried, and idempotency-keyed.

Webhooks are the at-least-once, off-machine counterpart to the presence-based event stream: instead of holding a connection open with dial wait-for, your server receives events as they happen.

Create a subscription

Create one from the dashboard Webhooks page, or via the API:

$curl -X POST https://api.getdial.ai/v1/webhooks \
> -H "Authorization: Bearer $DIAL_API_KEY" \
> -H "Content-Type: application/json" \
> -d '{ "targetUrl": "https://example.com/dial/webhook", "eventTypes": ["*"] }'
  • targetUrl must be HTTPS and must not point at a private, loopback, or link-local address.
  • eventTypes is either ["*"] (all events) or an explicit list of event types, e.g. ["message.received", "call.ended"].

The response includes a signing secret (whsec_…). The dashboard masks it and lets you copy it on demand; over the API the full secret is returned at creation and via GET /api/v1/webhooks/{id}/secret.

Delivery format

Each delivery is an HTTP POST to your targetUrl with the event JSON as the body and these headers:

HeaderValue
Content-Typeapplication/json
X-Dial-Event-TypeThe event type, e.g. message.received — use it to route/filter quickly.
X-Dial-Event-IDA stable per-event ID. Deduplicate on this — the same event may be delivered more than once.
X-Dial-Signaturet=<unix-seconds>,v1=<hex-hmac> — verify before trusting the body.

The body is the same event envelope you’d get from the event stream, plus a top-level id field equal to X-Dial-Event-ID.

Verify the signature

The signature protects against forged and replayed deliveries. Recompute the HMAC over {timestamp}.{raw_request_body} with your subscription secret and compare:

1import crypto from "node:crypto";
2
3// rawBody MUST be the exact bytes received, not a re-serialized object.
4function verifyDialWebhook(secret: string, header: string, rawBody: string): boolean {
5 const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
6 const t = parts.t;
7 const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
8 // constant-time compare
9 const ok = crypto.timingSafeEqual(Buffer.from(parts.v1, "hex"), Buffer.from(expected, "hex"));
10 // reject deliveries older than 5 minutes to limit replay
11 const fresh = Math.abs(Date.now() / 1000 - Number(t)) < 300;
12 return ok && fresh;
13}

Retries

Dial considers a delivery successful on any HTTP 2xx. Anything else — a non-2xx status, a connection error, or a timeout (10s per attempt) — is retried with exponential backoff, up to 6 attempts total (1 initial + 5 retries). Because deliveries retry, your endpoint must be idempotent: dedupe on X-Dial-Event-ID.

Test with a ping

The dashboard’s Fire ping button (and POST /api/v1/webhooks/{id}/ping) sends a webhook.ping event to that subscription, bypassing its eventTypes filter. When the ping is delivered and your endpoint returns 2xx, the dashboard records the time as Last ping. Use it to confirm a new endpoint is reachable and verifying signatures correctly.