Skip to main content

Receiving Webhooks

Register an HTTPS endpoint, verify each request, and handle retries and ordering. For the events themselves and their payloads, see the Webhook Events catalog.

Overview​

Webhook events are push notifications that DialStack sends to your server as things happen in the system. They are fire-and-forget notifications: return a 2xx to acknowledge receipt. The response body is ignored.

Webhook events are platform-scoped: register webhook endpoints on your platform, and DialStack delivers events for all accounts on that platform to the endpoints that match. An endpoint created with a DialStack-Account header is scoped to that account instead: it receives only that account's events, in addition to the platform's endpoints. The scope is fixed when the endpoint is created.

For browser-side notifications scoped to one account, see Real-Time Events (SSE).

Configuration​

Register one or more webhook endpoints with the Webhook Endpoints API. Each endpoint has:

  • a url that receives event POST requests: HTTPS on port 443 at a public address (redirects are not followed),
  • a secret used for signature verification (generated by DialStack and returned only in the create response — store it then, it cannot be retrieved later),
  • enabled_events, the list of event types it subscribes to (defaults to ["*"], all events),
  • a status (enabled or disabled).
Bash
curl https://api.dialstack.ai/v1/webhook_endpoints \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/webhooks",
"enabled_events": ["call.answered", "call.end"]
}'

Or with the SDK:

TypeScript
const endpoint = await dialstack.webhookEndpoints.create({
url: 'https://example.com/webhooks',
enabled_events: ['call.answered', 'call.end'], // omit for all events
});
console.log(endpoint.secret); // shown only once — store it

Sandbox vs live keys​

Webhook endpoints are environment-scoped, like the rest of the API. For an overview of sandbox vs. live keys and how the two environments are isolated, see Authentication. The environment is determined by the key you create the endpoint with and is fixed for the life of the endpoint (reported as livemode on the object):

  • An endpoint created with a live key (sk_live_...) receives events only from your live accounts.
  • An endpoint created with a sandbox key (sk_test_...) receives events only from your sandbox accounts.

This lets you point sandbox traffic at a separate backend: a sandbox account's events are never delivered to a live endpoint, and vice versa. If no endpoint matches an event's environment, the event is simply not delivered. Listing endpoints with a given key returns only that environment's endpoints.

Request format​

DialStack sends a POST request to each matching endpoint's url with the event as the JSON body. An event is delivered to an endpoint only when the endpoint's mode matches the originating account's mode and its enabled_events includes the event type (or is ["*"]). The body is the event envelope.

Headers:

HeaderDescription
Content-Typeapplication/json
X-DialStack-SignatureHMAC-SHA256 signature for verification
X-DialStack-Account-IdAccount where the event occurred
User-AgentDialStack-Webhook/1.0

Signature verification​

Every webhook request is signed using HMAC-SHA256 with the receiving endpoint's secret. Each endpoint has its own secret, so verify against the secret of the endpoint that received the request.

The same scheme signs every request DialStack sends to your servers, each with its own secret: voice app webhooks use the voice app's secret, and AI Scheduling requests use the secret of the agent's voice app.

The signature header format is:

Text
X-DialStack-Signature: t=1705312530,v1=a1b2c3d4e5f6...
  • t: Unix timestamp (seconds) when the request was signed
  • v1: HMAC-SHA256 signature, hex-encoded

To verify:

  1. Extract the timestamp (t) and signature (v1) from the header.
  2. Construct the signed payload: {timestamp}.{request_body}, using the raw request body exactly as received.
  3. Compute HMAC-SHA256 of the signed payload using your secret.
  4. Compare it with v1 using a constant-time comparison.
  5. Reject the request if the timestamp is more than 5 minutes old.

DialStack.webhooks.constructEvent is a static method: call it on the class, not on a client instance. It verifies the signature and the timestamp (5-minute tolerance by default), then returns the parsed body. It throws if verification fails.

JavaScript
import express from 'express';
import { DialStack } from '@dialstack/sdk-server';

const app = express();

app.post('/webhooks', express.raw({ type: 'application/json' }), (req, res) => {
let event;
try {
event = DialStack.webhooks.constructEvent(
req.body, // the raw Buffer, not parsed JSON
req.header('x-dialstack-signature'),
process.env.DIALSTACK_WEBHOOK_SECRET
);
} catch (err) {
return res.status(401).end();
}

res.status(200).end();
handleEvent(event); // process after acknowledging
});

Expected response​

Return any 2xx status code to acknowledge receipt. The response body is ignored. A non-2xx response, or no response within 30 seconds, counts as a failed delivery and is retried.

Retry behavior​

Failed deliveries are retried automatically:

  • Up to 5 delivery attempts in total, about 60 seconds apart.
  • A 30-second timeout per attempt.
  • After the last attempt fails, the event is not delivered to that endpoint.
  • A URL that breaks the requirements above (for example a port other than 443) is not retried: the event is dropped for that endpoint.

When an account has multiple matching endpoints, a retry re-delivers the event to all of them, so an endpoint that already responded 2xx may receive the event again. Combined with at-least-once delivery, this is why you should handle events idempotently using the id field.

An endpoint that fails 5 delivery attempts in a row (one event that exhausts its retries is enough) is marked unhealthy, and the platform is notified by email. While it is unhealthy, each event gets a single delivery attempt with no retries. The next successful delivery marks it healthy again.

Ordering​

Delivery preserves the order in which we publish events for an account. Events for different accounts may be delivered in parallel — do not assume ordering across accounts.

Publication order is not the same as the call's lifecycle order. call.* events are published as the call progresses, while recording.* and voicemail.* are published separately once the underlying media has been processed. The two families are not ordered relative to each other.

In particular, recording.available can arrive before call.end for the same call, because a recording tracks the conversation rather than the call: whenever a call outlives its conversation, the recording is complete and delivered while the call is still up. The common example is voicemail — the recording ends when the call reaches the mailbox, and call.end follows only when the caller hangs up, potentially tens of seconds later. Treat this as the general rule rather than a voicemail special case; other flows can separate the two the same way.

Correlate related events by call_id and treat each event as independently meaningful. Do not treat the arrival of one event type as evidence that another has already been delivered, and do not hold up processing of one family while waiting for the other.

Timing​

EventTypical Latency
call.initiated< 1 second
call.incoming< 1 second
call.ringing< 1 second
call.mobile_push_wakeup< 1 second
call.answered< 1 second
call.end< 1 second
call.transfer< 1 second
call.parked< 1 second
call.unparked< 1 second
queue.call.*< 1 second
voicemail.new< 2 seconds
recording.available< 5 seconds after the recording ends — the call ending, or the call reaching voicemail (see Ordering); on redaction-enabled accounts, after redaction completes (transcription + audio rewrite)
recording.failedOn redaction-enabled accounts, when redaction cannot complete
recording.transcription.complete~10 seconds after recording
recording.summary.complete~15 seconds after recording
voicemail.transcription.complete~10 seconds after voicemail
voicemail.summary.complete~15 seconds after voicemail
fax.received< 1 second after receipt completes
fax.delivered< 1 second after transmission completes
fax.failed< 1 second after the final attempt

Best practices​

  • Verify signatures on every request to prevent spoofing.
  • Return 2xx quickly and defer processing — long-running handlers risk timeouts.
  • Handle events idempotently using the id field — events may be delivered more than once.
  • Don't rely on strict cross-account ordering — events from different accounts may arrive in any order.
  • Correlate by call_id, not arrival order — recording.* and voicemail.* are not ordered against call.*, so recording.available may precede call.end (see Ordering).