Webhook Events
Receive real-time notifications about calls, recordings, voicemails, and transcriptions via HTTP webhooks.
Webhook Events is the backend transport for call events. For the browser transport, see Real-Time Events (SSE). For common patterns built on top, see Screen Pop and Activity Logging.
Overview
Webhook events are push notifications that DialStack sends to your server as things happen in the system. Unlike appointment webhooks (which are request/response), event webhooks are fire-and-forget notifications — return 200 to acknowledge receipt.
Webhook events are platform-scoped: configure a single webhook URL on your platform, and DialStack delivers events for all accounts on that platform.
For frontend real-time notifications (account-scoped), see Real-Time Events (SSE).
Configuration
To receive webhook events, configure a webhook_url on your platform via the platform management API. DialStack automatically generates and manages the webhook_secret used for signature verification.
All events for all accounts on the platform are delivered to this single URL.
Event Types
| Event | Description |
|---|---|
call.initiated | Outbound call started |
call.incoming | Inbound call received |
call.ringing | A user's phones are about to ring |
call.answered | Call answered |
call.end | Call ended |
call.transfer | Call transferred |
call.emergency | An emergency number was dialed (911 or regional equivalent) |
queue.call.queued | Caller entered a call queue |
queue.call.dispatched | Queue dispatched the caller to agent targets |
queue.call.answered | Queue caller was answered by an agent |
queue.call.abandoned | Caller hung up while waiting in queue |
queue.call.timed_out | Queue wait timed out before an agent answered |
queue.call.completed | Answered queue call completed |
queue.call.callback_requested | Caller requested a queue callback |
queue.call.callback_attempted | Queue callback attempt completed |
queue.call.callback_failed | Queue callback request reached a terminal failure |
voicemail.new | New voicemail received |
recording.available | Call recording ready for download |
recording.transcription.complete | Call transcript ready |
recording.summary.complete | AI call summary ready |
voicemail.transcription.complete | Voicemail transcript ready |
Event Envelope
All webhook events share this envelope structure:
{
"id": "evt_01jqr5k8m3n4p6q7r8s9t0u1v2",
"type": "call.end",
"created_at": "2026-01-15T14:35:30Z",
"account_id": "acct_01h2xcejqtf2nbrexx3vqjhp41",
"data": {}
}
| Field | Type | Description |
|---|---|---|
id | string | Unique event identifier. Use for idempotent processing. |
type | string | Event type (see table above). |
created_at | string | ISO 8601 timestamp when the event was created. |
account_id | string | Account where the event occurred. |
data | object | Event-specific payload (see below). |
Event Payloads
call.initiated
Sent when an outbound call starts dialing.
{
"id": "evt_01jqr5k8m3n4p6q7r8s9t0u1v2",
"type": "call.initiated",
"created_at": "2026-01-15T14:30:00Z",
"account_id": "acct_01h2xcejqtf2nbrexx3vqjhp41",
"data": {
"call_id": "call_01h2xcejqtf2nbrexx3vqjhp45",
"direction": "outbound",
"from_number": "+14155559876",
"to_number": "+14155551234",
"user_id": "user_01h2xcejqtf2nbrexx3vqjhp42",
"from_user_id": "user_01h2xcejqtf2nbrexx3vqjhp42",
"started_at": "2026-01-15T14:30:00Z"
}
}
call.incoming
Sent when an inbound call is received.
{
"id": "evt_01jqr5k8m3n4p6q7r8s9t0u1v3",
"type": "call.incoming",
"created_at": "2026-01-15T14:30:00Z",
"account_id": "acct_01h2xcejqtf2nbrexx3vqjhp41",
"data": {
"call_id": "call_01h2xcejqtf2nbrexx3vqjhp45",
"direction": "inbound",
"from_number": "+14155551234",
"from_name": "John Smith",
"to_number": "+14155559876",
"user_id": "user_01h2xcejqtf2nbrexx3vqjhp42",
"to_user_id": "user_01h2xcejqtf2nbrexx3vqjhp42",
"started_at": "2026-01-15T14:30:00Z"
}
}
| Field | Type | Description |
|---|---|---|
call_id | string | Call identifier |
direction | string | Always "inbound" |
from_number | string | Caller's phone number (E.164) |
from_name | string | null | Caller's display name |
to_number | string | Called phone number (E.164) |
user_id | string | null | User the call is routed to. Only set when the called number targets a single user directly; omitted when the call routes to a ring group, voice app, dial plan, or any other destination where a specific user is not known at this stage. |
from_user_id | string | null | Calling user (DIA-801). See User attribution below. |
to_user_id | string | null | Called user (DIA-801). See User attribution below. |
started_at | string | ISO 8601 timestamp |
call.ringing
Sent when a user's phones are about to ring. Fired once per (call, user) — a user with multiple phones (desk + mobile) produces a single event, not one per device. For a ring group or ring-all-users call, one event is emitted per user being rung, all sharing the same call_id. user_id always identifies the user whose phones are ringing, never the caller.
Not emitted for the initiating user's own phone ringing during a click-to-call (leg A).
{
"id": "evt_01jqr5k8m3n4p6q7r8s9t0u1v8",
"type": "call.ringing",
"created_at": "2026-01-15T14:30:01Z",
"account_id": "acct_01h2xcejqtf2nbrexx3vqjhp41",
"data": {
"call_id": "call_01h2xcejqtf2nbrexx3vqjhp45",
"user_id": "user_01h2xcejqtf2nbrexx3vqjhp42",
"from_number": "+14155551234",
"from_name": "John Smith",
"to_number": "+14155559876",
"ringing_at": "2026-01-15T14:30:01Z"
}
}
| Field | Type | Description |
|---|---|---|
call_id | string | Call identifier |
user_id | string | User whose phones are about to ring |
from_number | string | Caller's phone number (E.164) |
from_name | string | null | Caller's display name |
to_number | string | Called phone number (E.164) |
ringing_at | string | ISO 8601 timestamp when the ring began |
call.answered
Sent when a call is answered.
{
"id": "evt_01jqr5k8m3n4p6q7r8s9t0u1v4",
"type": "call.answered",
"created_at": "2026-01-15T14:30:05Z",
"account_id": "acct_01h2xcejqtf2nbrexx3vqjhp41",
"data": {
"call_id": "call_01h2xcejqtf2nbrexx3vqjhp45",
"direction": "inbound",
"from_number": "+14155551234",
"from_name": "John Smith",
"to_number": "+14155559876",
"user_id": "user_01h2xcejqtf2nbrexx3vqjhp42",
"to_user_id": "user_01h2xcejqtf2nbrexx3vqjhp42",
"started_at": "2026-01-15T14:30:00Z",
"answered_at": "2026-01-15T14:30:05Z"
}
}
call.end
Sent when a call ends.
{
"id": "evt_01jqr5k8m3n4p6q7r8s9t0u1v5",
"type": "call.end",
"created_at": "2026-01-15T14:35:30Z",
"account_id": "acct_01h2xcejqtf2nbrexx3vqjhp41",
"data": {
"call_id": "call_01h2xcejqtf2nbrexx3vqjhp45",
"direction": "inbound",
"from_number": "+14155551234",
"from_name": "John Smith",
"to_number": "+14155559876",
"user_id": "user_01h2xcejqtf2nbrexx3vqjhp42",
"to_user_id": "user_01h2xcejqtf2nbrexx3vqjhp42",
"started_at": "2026-01-15T14:30:00Z",
"answered_at": "2026-01-15T14:30:05Z",
"ended_at": "2026-01-15T14:35:30Z",
"duration_seconds": 325,
"status": "completed"
}
}
status value | Description |
|---|---|
completed | Call was answered and ended normally |
no-answer | Call rang but was not answered |
busy | Callee was busy or rejected the call |
failed | Call failed due to a network or system error |
voicemail | Call went to voicemail |
direction value | Description |
|---|---|
inbound | Call from PSTN to a DID |
outbound | Call from an endpoint to PSTN |
internal | Extension-to-extension or feature code |
user_id is populated with the user most closely associated with the call (the caller for outbound, the answerer for inbound and internal) and is omitted when no single user was involved — for example, an unanswered inbound ring group call or a call handled entirely by a voice app.
The generic call.* events are queue-agnostic; queue context is exposed via the dedicated queue.call.* events below. Subscribe to both streams and correlate by call_id when you need queue analytics alongside generic call state.
Queue call lifecycle
Queue lifecycle events are emitted in addition to the generic call.* lifecycle. Use them when you need queue-specific analytics or routing decisions without inferring queue state from call.end.
Common fields:
| Field | Type | Description |
|---|---|---|
call_id | string | Call identifier |
queue_id | string | Queue identifier |
queue_name | string | Queue display name |
from_number | string | Caller's phone number (E.164) |
from_name | string | null | Caller's display name |
to_number | string | Called phone number (E.164) |
position_at_admit | number | Caller position when admitted to the queue, omitted when unavailable |
wait_seconds | number | Seconds spent waiting in queue before this outcome |
Event-specific fields:
| Event | Additional fields |
|---|---|
queue.call.queued | queued_at |
queue.call.dispatched | agents_claimed, targets_dispatched, dispatched_at |
queue.call.answered | agent_user_id, agent_endpoint_id, answered_at |
queue.call.abandoned | abandoned_at |
queue.call.timed_out | timed_out_at |
queue.call.completed | agent_user_id, agent_endpoint_id, completed_at |
queue.call.callback_requested | callback_id, original_priority, original_entered_at, requested_at |
queue.call.callback_attempted | callback_id, attempt_number, result, next_attempt_at |
queue.call.callback_failed | callback_id, attempts, reason |
User attribution
call.initiated, call.incoming, call.answered, and call.end also carry from_user_id and to_user_id so both sides of a call are visible — most usefully on internal user-to-user calls, where user_id alone only identifies the answerer. Both fields are omitted when the side in question is not a user on this account (for example, an external PSTN caller has no from_user_id).
| Call type | from_user_id | to_user_id |
|---|---|---|
| Inbound PSTN → user | omitted (external caller) | The routed user |
| Inbound PSTN → ring group, answered | omitted | The answering member |
| Inbound PSTN → voice app or dial plan | omitted | omitted (until a user is reached) |
| Inbound PSTN → user with Find Me / Follow Me | omitted | The Find Me / Follow Me owner |
| Outbound from an endpoint | The endpoint's user | omitted (external destination) |
| Internal user-to-user | The calling user | The called user |
| Click-to-call | The initiating user | The answering endpoint's user, when applicable |
user_id is preserved with its original semantics as a convenience alias for existing integrations; new integrations should prefer from_user_id and to_user_id.
call.transfer
Sent when a call is transferred.
{
"id": "evt_01jqr5k8m3n4p6q7r8s9t0u1v6",
"type": "call.transfer",
"created_at": "2026-01-15T14:32:00Z",
"account_id": "acct_01h2xcejqtf2nbrexx3vqjhp41",
"data": {
"call_id": "call_01h2xcejqtf2nbrexx3vqjhp45",
"direction": "inbound",
"from_number": "+14155551234",
"to_number": "+14155559876",
"transferred_to": "1001",
"transferred_by": "user_01h2xcejqtf2nbrexx3vqjhp42",
"transferred_at": "2026-01-15T14:32:00Z"
}
}
call.emergency
Sent when an emergency number is dialed from any endpoint on the account. Subscribers can route this event to a paging system, security desk, or any other on-call destination.
The event also fires for carrier emergency test numbers (such as 933 in the US) so you can verify your wiring end-to-end without contacting an actual public safety answering point.
e911_provisioned is false when the dialing endpoint had no provisioned dispatchable location at the time of the call. In that case location_id and location_name are omitted from the payload. Treat that combination as a signal that an emergency call went out without an accurate address — the account holder should provision the location before the next call.
{
"id": "evt_01jqr5k8m3n4p6q7r8s9t0u1v8",
"type": "call.emergency",
"created_at": "2026-05-03T18:30:00Z",
"account_id": "acct_01h2xcejqtf2nbrexx3vqjhp41",
"data": {
"call_id": "call_01h2xcejqtf2nbrexx3vqjhp45",
"to_number": "911",
"user_id": "user_01h2xcejqtf2nbrexx3vqjhp47",
"device_id": "dev_01h2xcejqtf2nbrexx3vqjhp49",
"extension": "1001",
"location_id": "loc_01h2xcejqtf2nbrexx3vqjhp48",
"location_name": "Headquarters",
"location_address": "123 Main St, Springfield, IL 62701, US",
"e911_provisioned": true,
"placed_at": "2026-05-03T18:30:00Z"
}
}
voicemail.new
Sent when a new voicemail is received. Use GET /v1/voicemails/{voicemail_id} to retrieve the full voicemail details and audio.
{
"id": "evt_01jqr5k8m3n4p6q7r8s9t0u1v7",
"type": "voicemail.new",
"created_at": "2026-01-15T14:36:00Z",
"account_id": "acct_01h2xcejqtf2nbrexx3vqjhp41",
"data": {
"voicemail_id": "vm_01h2xcejqtf2nbrexx3vqjhp46",
"user_id": "user_01h2xcejqtf2nbrexx3vqjhp42",
"from_number": "+14155551234",
"from_name": "John Smith",
"duration_seconds": 42,
"created_at": "2026-01-15T14:36:00Z"
}
}
user_id is included when the voicemail belongs to a single user's mailbox and is omitted when it belongs to a shared voicemail box.
recording.available
Sent when a call recording is ready for download. Use GET /v1/calls/{call_id}/recording to download the audio file.
{
"id": "evt_01jqr5k8m3n4p6q7r8s9t0u1v8",
"type": "recording.available",
"created_at": "2026-01-15T14:35:35Z",
"account_id": "acct_01h2xcejqtf2nbrexx3vqjhp41",
"data": {
"call_id": "call_01h2xcejqtf2nbrexx3vqjhp45",
"duration_seconds": 325
}
}
recording.transcription.complete
Sent when a call transcription is ready. Use GET /v1/calls/{call_id}/transcript to retrieve the full transcript text.
{
"id": "evt_01jqr5k8m3n4p6q7r8s9t0u1v9",
"type": "recording.transcription.complete",
"created_at": "2026-01-15T14:35:45Z",
"account_id": "acct_01h2xcejqtf2nbrexx3vqjhp41",
"data": {
"call_id": "call_01h2xcejqtf2nbrexx3vqjhp45",
"status": "completed"
}
}
recording.summary.complete
Sent when an AI-generated call summary is ready. Use GET /v1/calls/{call_id}/transcript to retrieve the summary.
{
"id": "evt_01jqr5k8m3n4p6q7r8s9t0u1va",
"type": "recording.summary.complete",
"created_at": "2026-01-15T14:36:00Z",
"account_id": "acct_01h2xcejqtf2nbrexx3vqjhp41",
"data": {
"call_id": "call_01h2xcejqtf2nbrexx3vqjhp45"
}
}
voicemail.transcription.complete
Sent when a voicemail transcription is ready. Use GET /v1/voicemails/{voicemail_id}/transcript to retrieve the transcript text.
{
"id": "evt_01jqr5k8m3n4p6q7r8s9t0u1vb",
"type": "voicemail.transcription.complete",
"created_at": "2026-01-15T14:36:10Z",
"account_id": "acct_01h2xcejqtf2nbrexx3vqjhp41",
"data": {
"voicemail_id": "vm_01h2xcejqtf2nbrexx3vqjhp46",
"user_id": "user_01h2xcejqtf2nbrexx3vqjhp42",
"status": "completed"
}
}
Webhook Protocol
Request Format
DialStack sends a POST request to your platform's configured webhook URL with the event as the JSON body.
Headers:
| Header | Description |
|---|---|
Content-Type | application/json |
X-DialStack-Signature | HMAC-SHA256 signature for verification |
X-DialStack-Account-Id | Account where the event occurred |
User-Agent | DialStack-Webhook/1.0 |
Signature Verification
Every webhook request is signed using HMAC-SHA256 with your platform's webhook secret. The signature header format is:
X-DialStack-Signature: t=1705312530,v1=a1b2c3d4e5f6...
To verify:
- Extract the timestamp (
t) and signature (v1) from the header - Construct the signed payload:
{timestamp}.{request_body} - Compute HMAC-SHA256 of the signed payload using your webhook secret
- Compare with the provided signature
For detailed verification examples in Node.js, Python, and Go, see Appointment Webhooks — Signature Verification.
Expected Response
Return any 2xx status code to acknowledge receipt. The response body is ignored. Non-2xx responses trigger a retry.
Retry Behavior
Failed deliveries (non-2xx response or timeout) are retried automatically:
- Up to 5 retry attempts at approximately 60-second intervals
- 30-second timeout per delivery attempt
- After all retries are exhausted, the event is moved to a dead letter queue for investigation
Ordering
Events for the same account are delivered in order. Events for different accounts may be delivered in parallel. Do not assume ordering across accounts.
Timing
| Event | Typical Latency |
|---|---|
call.initiated | < 1 second |
call.incoming | < 1 second |
call.ringing | < 1 second |
call.answered | < 1 second |
call.end | < 1 second |
call.transfer | < 1 second |
queue.call.* | < 1 second |
voicemail.new | < 2 seconds |
recording.available | < 5 seconds after call ends |
recording.transcription.complete | ~10 seconds after recording |
recording.summary.complete | ~15 seconds after recording |
voicemail.transcription.complete | ~10 seconds after voicemail |
Best Practices
- Verify signatures on every request to prevent spoofing
- Return
200quickly and defer processing — long-running handlers risk timeouts - Handle events idempotently using the
idfield — events may be delivered more than once - Don't rely on strict cross-account ordering — events from different accounts may arrive in any order
Related Resources
- Real-Time Events (SSE) — Frontend event streaming
- Appointment Webhooks — Scheduling webhook protocol
- Voice Apps — Programmable voice webhooks