Webhook Events
Every event DialStack can send to your webhook endpoints, and the payload each one carries: calls, queues, recordings, voicemails, transcriptions, faxes, devices, billable counts, and port orders.
To register an endpoint, verify signatures, and handle retries and ordering, see Receiving Webhooks. Webhooks are 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.
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.mobile_push_wakeup | Send a push to wake the user's mobile app for this call |
call.answered | Call answered |
call.end | Call ended |
call.transfer | Call transferred |
call.parked | Call placed in a park slot |
call.unparked | Park slot emptied: retrieved, abandoned, or timed out |
call.emergency | An emergency number was dialed (911 or regional equivalent) |
call.command_succeeded | A call-control command completed successfully |
call.command_failed | A call-control command did not complete |
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 stopped holding a caller before an agent answered |
queue.call.exited | Caller pressed the queue's exit key 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 |
fax.received | Inbound fax received |
fax.delivered | Outbound fax delivered successfully |
fax.failed | Outbound fax failed to send |
voicemail.new | New voicemail received |
recording.available | Call recording ready for download |
recording.failed | Call recording could not be prepared (e.g. redaction failed) |
recording.transcription.complete | Call transcript ready |
recording.summary.complete | AI call summary and sentiment ready |
recording.deleted | Call recording audio deleted |
recording.transcript.deleted | Call transcript, summary and sentiment deleted |
voicemail.transcription.complete | Voicemail transcript ready |
voicemail.summary.complete | AI voicemail summary and sentiment ready |
device.provisioned | Deskphone or DECT base completed its first provisioning |
device.config_fetched | A device fetched its configuration (every boot / check-sync) |
device.provisioning_failed | A device's configuration could not be generated |
device.hot_desk.started | A user logged in on a shared phone |
device.hot_desk.ended | A hot-desk session ended |
user.created | A user was created and is now billable |
user.deleted | A user was deleted and is no longer billable |
phone_number.activated | A phone number went into service and is now billable |
phone_number.disconnected | A phone number left service and is no longer billable |
port_order.updated | A port order changed status or its port date moved |
Event Envelope
All webhook events share this envelope structure:
{
"id": "evt_01jqr5k8m3n4p6q7r8s9t0u1v2",
"type": "call.end",
"created_at": "2026-01-15T14:35:30Z",
"account": "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 | string | Account where the event occurred. |
data | object | Event-specific payload (see below). |
The envelope also carries account_id, with the same value as account. It is a deprecated alias kept for existing integrations; read account in new code.
Reference field names
Fields that point at another object are named after that object: user, from_user, to_user, voicemail, fax, device, location, queue, and so on. During a transition, most payloads also carry the same value under the older _id key (user_id, from_user_id, voicemail_id, fax_id, …). Those keys are deprecated aliases kept for existing integrations; use the names shown on this page in new code. call_id is not an alias and stays as it is.
The same call events are also delivered over the Real-Time Events (SSE) stream, but without this envelope: SSE payloads are flat (no id/created_at/data wrapper) and name the event-type field event rather than type. Treat the two transports as separate schemas — see Real-Time Events for the SSE payloads.
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": "acct_01h2xcejqtf2nbrexx3vqjhp41",
"data": {
"call_id": "call_01h2xcejqtf2nbrexx3vqjhp45",
"direction": "outbound",
"from_number": "+14155559876",
"to_number": "+14155551234",
"user": "user_01h2xcejqtf2nbrexx3vqjhp42",
"from_user": "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": "acct_01h2xcejqtf2nbrexx3vqjhp41",
"data": {
"call_id": "call_01h2xcejqtf2nbrexx3vqjhp45",
"direction": "inbound",
"from_number": "+14155551234",
"from_name": "John Smith",
"to_number": "+14155559876",
"user": "user_01h2xcejqtf2nbrexx3vqjhp42",
"to_user": "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 | string | 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 | string | Calling user, when it is a user on this account. See User attribution below. |
to_user | string | Called user, when known. 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 always identifies the user whose phones are ringing, never the caller.
Not emitted when the initiating user's own phone rings at the start of a click-to-call.
{
"id": "evt_01jqr5k8m3n4p6q7r8s9t0u1v8",
"type": "call.ringing",
"created_at": "2026-01-15T14:30:01Z",
"account": "acct_01h2xcejqtf2nbrexx3vqjhp41",
"data": {
"call_id": "call_01h2xcejqtf2nbrexx3vqjhp45",
"user": "user_01h2xcejqtf2nbrexx3vqjhp42",
"direction": "inbound",
"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 | string | User whose phones are about to ring |
direction | string | "inbound", "outbound" or "internal". Absent if the platform could not determine the direction |
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.mobile_push_wakeup
Sent when an incoming call is being delivered to the user's web/mobile calling session and the user has mobile_push_wakeup enabled. This is your cue to send the push notification that wakes the user's mobile app: when the app has no active session, the call is held for a wake-up window while your push → app wake-up → connect flow completes.
Unlike call.ringing (which fires whenever a user is being reached, including when their calls forward to an external number), this event fires only when the user's own app session is actually being rung — every push it triggers corresponds to a real, answerable call. Use call.ringing for screen pop; use this event for mobile push delivery.
{
"id": "evt_01jqr5k8m3n4p6q7r8s9t0u1v9",
"type": "call.mobile_push_wakeup",
"created_at": "2026-01-15T14:30:01Z",
"account": "acct_01h2xcejqtf2nbrexx3vqjhp41",
"data": {
"call_id": "call_01h2xcejqtf2nbrexx3vqjhp45",
"user": "user_01h2xcejqtf2nbrexx3vqjhp42",
"direction": "inbound",
"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 | string | User whose app should be woken |
direction | string | "inbound", "outbound" or "internal". Absent if the platform could not determine the direction |
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 delivery began |
call.answered
Sent when a call is answered.
{
"id": "evt_01jqr5k8m3n4p6q7r8s9t0u1v4",
"type": "call.answered",
"created_at": "2026-01-15T14:30:05Z",
"account": "acct_01h2xcejqtf2nbrexx3vqjhp41",
"data": {
"call_id": "call_01h2xcejqtf2nbrexx3vqjhp45",
"direction": "inbound",
"from_number": "+14155551234",
"from_name": "John Smith",
"to_number": "+14155559876",
"user": "user_01h2xcejqtf2nbrexx3vqjhp42",
"to_user": "user_01h2xcejqtf2nbrexx3vqjhp42",
"started_at": "2026-01-15T14:30:00Z",
"answered_at": "2026-01-15T14:30:05Z",
"connected_at": "2026-01-15T14:30:47Z"
}
}
answered_at is the signalling answer — a greeting, menu, or voice app answering the media path counts, so for an inbound call that plays one it is roughly started_at, not when a person picked up. connected_at is when the call connected: a user's device or an external number answered it, or a voice app took it with attach or bridge. It is null when the call is answered by voicemail. For a direct call with no greeting the two coincide.
call.end
Sent when a call ends.
{
"id": "evt_01jqr5k8m3n4p6q7r8s9t0u1v5",
"type": "call.end",
"created_at": "2026-01-15T14:35:30Z",
"account": "acct_01h2xcejqtf2nbrexx3vqjhp41",
"data": {
"call_id": "call_01h2xcejqtf2nbrexx3vqjhp45",
"direction": "inbound",
"from_number": "+14155551234",
"from_name": "John Smith",
"to_number": "+14155559876",
"user": "user_01h2xcejqtf2nbrexx3vqjhp42",
"to_user": "user_01h2xcejqtf2nbrexx3vqjhp42",
"started_at": "2026-01-15T14:30:00Z",
"answered_at": "2026-01-15T14:30:05Z",
"connected_at": "2026-01-15T14:30:47Z",
"ended_at": "2026-01-15T14:35:30Z",
"duration_seconds": 325,
"status": "completed"
}
}
started_at is when the call was initiated, the same value the call log reports for it. duration_seconds runs from answered_at to ended_at, so ring time is not included, and it is 0 for a call that was never answered.
connected_at is null when the call never connected: the caller hung up during the greeting, menu or queue or while ringing, or the call was answered by voicemail (status: "voicemail"). This lets you distinguish a genuinely-handled call from one that only ever reached the platform.
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 an outside number to one of the account's phone numbers |
outbound | Call from a user's phone to an outside number |
internal | Extension-to-extension or feature code |
user is populated with the user most closely associated with the call: the caller for outbound; for inbound and internal, the user who answered — or, when the call ended unanswered, the single user whose phones were being rung (so a missed call is attributed to the user who missed it). It is omitted when no single user was involved — for example, an unanswered inbound ring group call, a call handled entirely by a voice app, or a dial plan call answered by a forward to an external number.
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 | 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 |
The three callback_* events are the exception: a callback is recorded against
the queue rather than a live call, so they carry queue and the caller's number
but not call_id, from_name, position_at_admit or wait_seconds, and only
callback_requested carries queue_name.
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, agent_endpoint, answered_at |
queue.call.abandoned | abandoned_at |
queue.call.timed_out | timed_out_at |
queue.call.exited | exit_key, exited_at |
queue.call.completed | agent_user, agent_endpoint, completed_at |
queue.call.callback_requested | callback, original_priority, original_entered_at, requested_at |
queue.call.callback_attempted | callback, attempt_number, result, next_attempt_at |
queue.call.callback_failed | callback, attempts, reason |
queue, agent_user, agent_endpoint and callback are also sent under the
older queue_id, agent_user_id, agent_endpoint_id and callback_id keys.
Those are deprecated aliases kept for existing integrations; use the canonical
names above in new code.
queue.call.timed_out fires when the timeout behavior runs for an unanswered caller, whether because their Max wait elapsed or because the team is no longer staffed enough to hold them. A caller who pressed the exit key gets queue.call.exited instead.
User attribution
call.initiated, call.incoming, call.answered, and call.end also carry from_user and to_user so both sides of a call are visible — most usefully on internal user-to-user calls, where user 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 caller has no from_user).
| Call type | from_user | to_user |
|---|---|---|
| Inbound external → user | omitted (external caller) | The routed user |
| Inbound external → ring group, answered | omitted | The answering member |
| Inbound external → voice app or dial plan | omitted | omitted (until a user is rung — see below) |
| Inbound external → user with Find Me / Follow Me | omitted | The Find Me / Follow Me owner |
| Outbound from a user's phone | That user | omitted (external destination) |
| Internal user-to-user | The calling user | The called user |
| Click-to-call | The initiating user | The answering user, when applicable |
On a dial plan call, to_user (and user) track the user step currently being rung, exactly as if that user had been called directly: set while a user's phones are ringing — and kept if the caller hangs up during that ring, so the missed call is attributed to them — then replaced by the answering user when the call is answered, and cleared when the plan moves past an unanswered step or a forward to an external number answers the call. A dial plan call answered by an external number therefore carries no user or to_user.
user keeps its original single-user meaning for existing integrations; new integrations should prefer from_user and to_user.
call.transfer
Sent when a call is transferred. A transfer always involves two calls, and each one receives its own webhooks, so you can track both sides. How the second call behaves depends on the transfer type:
- Blind (cold) transfer — the caller is sent to a new destination without a consultation. This creates a brand-new call that runs its full lifecycle independently, including its own
call.endwhen it finishes. You receivecall.transferon the original call plus a complete set of events for the new call. - Warm (attended) transfer — you first place a consultation call to the target, then complete the transfer. On completion the consultation call is merged into the primary call rather than hung up. Because it does not end on its own, the consultation call emits
call.initiatedandcall.answeredbut notcall.end— itscall.transferevent is its final event. The primary call continues (now connected to the transfer target) and ends normally with its owncall.end.
Both calls also appear in your call logs. To relate the two calls in a transfer, use related_call, which references the other call in the pair (each call's event points at the other). It is populated whenever the two calls can be linked; if it is null, correlate them using the phone numbers and timestamps.
{
"id": "evt_01jqr5k8m3n4p6q7r8s9t0u1v6",
"type": "call.transfer",
"created_at": "2026-01-15T14:32:00Z",
"account": "acct_01h2xcejqtf2nbrexx3vqjhp41",
"data": {
"call_id": "call_01h2xcejqtf2nbrexx3vqjhp45",
"direction": "inbound",
"from_number": "+14155551234",
"to_number": "+14155559876",
"transferred_to": "1001",
"transferred_by": "user_01h2xcejqtf2nbrexx3vqjhp42",
"related_call": "call_01h2xcejqtf2nbrexx3vqjhp47",
"transferred_at": "2026-01-15T14:32:00Z"
}
}
call.parked
Sent when a caller is placed in a park slot, whichever way the call was parked. It is normally followed by one call.unparked for the same call and slot. See Call parking lifecycle for how the two fit together.
| Field | Type | Description |
|---|---|---|
call | string | The parked caller's call — the same ID other call.* events report as call_id and the call logs report as id. |
slot | integer | The park slot the call occupies — the N in *68N. |
parked_by | string | null | The user who parked the call. null when the phone that parked it has no assigned user. |
parked_by_extension | string | null | That user's extension. null when they have none. |
parked_by_device_type | string | The kind of phone that parked the call: softphone, deskphone, dect_handset, or other (a phone with no provisioned device, or a device that is none of the above). |
parked_at | string | When the call was parked (ISO 8601). |
rings_back_at | string | When the parker's phones will ring back if nobody retrieves the call first (ISO 8601). |
direction | string | The parked call's direction: inbound, outbound, or internal. |
to_number | string | The parked call's original called number. |
{
"id": "evt_01jqr5k8m3n4p6q7r8s9t0u1w1",
"type": "call.parked",
"created_at": "2026-01-15T14:32:00Z",
"account": "acct_01h2xcejqtf2nbrexx3vqjhp41",
"data": {
"call": "call_01h2xcejqtf2nbrexx3vqjhp45",
"slot": 1,
"parked_by": "user_01h2xcejqtf2nbrexx3vqjhp42",
"parked_by_extension": "1001",
"parked_by_device_type": "softphone",
"parked_at": "2026-01-15T14:32:00Z",
"rings_back_at": "2026-01-15T14:37:00Z",
"direction": "inbound",
"to_number": "+14155559876"
}
}
call.unparked
Sent when a park slot empties, for any reason. cleared_reason says why:
retrieved— someone picked the call back up, either by dialing*68N(or pressing the slot's BLF key) or by answering the ring-back.abandoned— the parked caller hung up.timeout— the call was ended by the system rather than picked up: normally because the ring-back went unanswered.
The retrieved_by fields are present only when cleared_reason is retrieved, and name whoever picked the call up. retrieved_by is additionally omitted when the phone that retrieved the call has no assigned user.
Retrieving by dialing *68N is also a call in its own right, from the retrieving phone: it produces call.answered and, when it hangs up, call.end, both with related_call set to the parked call.
| Field | Type | Description |
|---|---|---|
call | string | The parked caller's call, as on call.parked. |
slot | integer | The slot that emptied. |
cleared_reason | string | retrieved, abandoned, or timeout. |
retrieved_by | string | The user who retrieved the call. retrieved only. |
retrieved_by_extension | string | That user's extension. retrieved only, and only when they have one. |
retrieved_by_device_type | string | The kind of phone that retrieved the call, with the same values as parked_by_device_type. retrieved only. |
unparked_at | string | When the slot emptied (ISO 8601). |
{
"id": "evt_01jqr5k8m3n4p6q7r8s9t0u1w2",
"type": "call.unparked",
"created_at": "2026-01-15T14:33:10Z",
"account": "acct_01h2xcejqtf2nbrexx3vqjhp41",
"data": {
"call": "call_01h2xcejqtf2nbrexx3vqjhp45",
"slot": 1,
"cleared_reason": "retrieved",
"retrieved_by": "user_01h2xcejqtf2nbrexx3vqjhp43",
"retrieved_by_extension": "1002",
"retrieved_by_device_type": "deskphone",
"unparked_at": "2026-01-15T14:33:10Z"
}
}
Call parking lifecycle
call.parked and call.unparked are the supported way to follow a parked call. Pair them on call and slot:
- A user parks the call →
call.parked. - The slot empties →
call.unparked, with acleared_reason.
A call that is parked again after being retrieved produces a new pair: call.unparked (retrieved) for the first slot, then call.parked for the next one. There is no separate re-park reason.
A park also still produces call.transfer, because parking a call is a transfer to the slot. That event names the slot in transferred_to (*68N), and integrations built before call.parked existed may match on it. That keeps working, but new integrations should use call.parked / call.unparked, which also cover retrieves and every way a slot can empty.
For what is parked right now — for example, to draw a board of slots when your app starts — read the slots from the Presence API, which reports state rather than events. Both surfaces use the same call ID and slot number, so the events and the board stitch together directly.
Don't treat the pair as a guarantee. In rare cases, such as a platform update landing while a call is parked, one side of a pair can be missing: a call.unparked with no earlier call.parked, or a call.parked whose call.unparked never arrives. If you keep a slot board, reconcile it against the Presence API from time to time rather than relying on the events alone.
If a webhook endpoint subscribes to specific event types, add call.parked and call.unparked to its enabled_events. Endpoints subscribed to ["*"] receive them automatically.
call.emergency
Sent when an emergency number is dialed from any phone 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 phone had no provisioned dispatchable location at the time of the call. In that case location, location_name and location_address 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.
e911_provisioned is omitted entirely when the emergency call was placed via a transfer. On a transfer to an emergency number the platform deliberately attaches no dispatchable location — it cannot know whether the transferring or transferred party's address is correct — and lets the public safety answering point resolve the location. The field has no meaning in that case, so its absence (rather than false) distinguishes a transfer from an unprovisioned direct call.
{
"id": "evt_01jqr5k8m3n4p6q7r8s9t0u1v8",
"type": "call.emergency",
"created_at": "2026-05-03T18:30:00Z",
"account": "acct_01h2xcejqtf2nbrexx3vqjhp41",
"data": {
"call_id": "call_01h2xcejqtf2nbrexx3vqjhp45",
"to_number": "911",
"user": "user_01h2xcejqtf2nbrexx3vqjhp47",
"device": "dev_01h2xcejqtf2nbrexx3vqjhp49",
"extension": "1001",
"location": "loc_01h2xcejqtf2nbrexx3vqjhp48",
"location_name": "Headquarters",
"location_address": "123 Main St, Springfield, IL 62701, US",
"e911_provisioned": true,
"placed_at": "2026-05-03T18:30:00Z"
}
}
| Field | Type | Description |
|---|---|---|
call_id | string | Call identifier |
to_number | string | The emergency number dialed, for example 911 or 933 |
user | string | The user who placed the call. Omitted when the phone has no assigned user |
device | string | The deskphone or DECT handset that placed the call. Omitted for softphones and other callers without a provisioned device |
extension | string | The caller's extension. Omitted when there is none |
location | string | The location whose address was sent with the call. Omitted when e911_provisioned is false or absent |
location_name | string | That location's name. Omitted with location |
location_address | string | That location's address on one line. Omitted with location |
e911_provisioned | boolean | Whether the call went out with a provisioned dispatchable location. Omitted for an emergency call placed by transfer (above) |
placed_at | string | When the call was placed (ISO 8601) |
user, device and location are also sent under the deprecated user_id, device_id and location_id keys.
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": "acct_01h2xcejqtf2nbrexx3vqjhp41",
"data": {
"voicemail": "vm_01h2xcejqtf2nbrexx3vqjhp46",
"user": "user_01h2xcejqtf2nbrexx3vqjhp42",
"call_id": "call_01h2xcejqtf2nbrexx3vqjhp45",
"from_number": "+14155551234",
"from_name": "John Smith",
"duration_seconds": 42,
"created_at": "2026-01-15T14:36:00Z"
}
}
user is included when the voicemail belongs to a single user's mailbox and is omitted when it belongs to a shared voicemail box. call_id identifies the call that produced the voicemail — use it to correlate with GET /v1/calls/{call_id} — and is omitted when the call could not be resolved.
recording.available
Sent when a call recording is ready for download. Use GET /v1/calls/{call_id}/recording to download the audio file.
A recording covers the conversation, which does not always run to the end of the call — so this event can arrive before call.end, and you should not assume it means the call is over. The common example is a call that reaches voicemail: the recording stops at that point, and the voicemail message itself is delivered separately via voicemail.new. See Ordering.
{
"id": "evt_01jqr5k8m3n4p6q7r8s9t0u1v8",
"type": "recording.available",
"created_at": "2026-01-15T14:35:35Z",
"account": "acct_01h2xcejqtf2nbrexx3vqjhp41",
"data": {
"call_id": "call_01h2xcejqtf2nbrexx3vqjhp45",
"duration_seconds": 325
}
}
recording.failed
Sent when a call recording could not be prepared and will never become available. On accounts with PII redaction enabled, recording.available is withheld until redaction succeeds; if redaction cannot complete, the audio is discarded and this event is sent instead so you are not left waiting for a recording that will never arrive. No recording is retrievable for the call.
{
"id": "evt_01jqr5k8m3n4p6q7r8s9t0u1w0",
"type": "recording.failed",
"created_at": "2026-01-15T14:35:40Z",
"account": "acct_01h2xcejqtf2nbrexx3vqjhp41",
"data": {
"call_id": "call_01h2xcejqtf2nbrexx3vqjhp45",
"error_code": "audio_redaction_failed",
"reason": "audio could not be redacted"
}
}
Branch on error_code, not the free-form reason:
error_code | Meaning |
|---|---|
audio_redaction_failed | The audio could not be redacted (unreadable recording, or sensitive speech that could not be located in the audio) |
transcription_failed | The call could not be transcribed, so sensitive speech could not be found |
internal_error | An internal error prevented the recording from being processed |
reason is an optional human-readable sentence for logging; new codes may be added, so treat an unrecognized error_code as a generic failure.
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": "acct_01h2xcejqtf2nbrexx3vqjhp41",
"data": {
"call_id": "call_01h2xcejqtf2nbrexx3vqjhp45",
"status": "completed"
}
}
recording.summary.complete
Sent when the AI summary and sentiment for a call transcript are ready. Both
are derived from the same transcript and committed together, so one event
signals both. The summary is
on the call record — GET /v1/calls/{call_id} —
and sentiment appears there as well as on
GET /v1/calls/{call_id}/transcript.
Both are best-effort, so the event can fire with only one of the two present.
It also fires when the transcript is too short to be worth summarizing, roughly
a sentence or two, with both summary and sentiment left null. In that case
the transcript itself is the summary, so read the fields rather than assuming
they are set.
When the AI step fails outright, no event is sent at all, so don't block a
call's processing on this one — use recording.transcription.complete for
that.
{
"id": "evt_01jqr5k8m3n4p6q7r8s9t0u1va",
"type": "recording.summary.complete",
"created_at": "2026-01-15T14:36:00Z",
"account": "acct_01h2xcejqtf2nbrexx3vqjhp41",
"data": {
"call_id": "call_01h2xcejqtf2nbrexx3vqjhp45"
}
}
recording.deleted
Sent when a call's recorded audio is deleted with
DELETE /v1/calls/{call_id}/recording.
If you keep a copy of the audio, delete it too. The transcript is not
affected; recording.transcript.deleted reports that separately.
{
"id": "evt_01jqr5k8m3n4p6q7r8s9t0u1vb",
"type": "recording.deleted",
"created_at": "2026-01-16T09:12:00Z",
"account": "acct_01h2xcejqtf2nbrexx3vqjhp41",
"data": {
"call_id": "call_01h2xcejqtf2nbrexx3vqjhp45"
}
}
recording.transcript.deleted
Sent when a call's transcript, summary and sentiment are deleted with
DELETE /v1/calls/{call_id}/transcript.
If you keep a copy of any of them, delete it too. The recording is not
affected; recording.deleted reports that separately.
{
"id": "evt_01jqr5k8m3n4p6q7r8s9t0u1vc",
"type": "recording.transcript.deleted",
"created_at": "2026-01-16T09:12:30Z",
"account": "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": "acct_01h2xcejqtf2nbrexx3vqjhp41",
"data": {
"voicemail": "vm_01h2xcejqtf2nbrexx3vqjhp46",
"user": "user_01h2xcejqtf2nbrexx3vqjhp42",
"call_id": "call_01h2xcejqtf2nbrexx3vqjhp45",
"status": "completed"
}
}
call_id identifies the call that produced the voicemail and is omitted when the call could not be resolved.
voicemail.summary.complete
Sent when the AI summary and sentiment for a voicemail transcript are ready.
Both are derived from the same transcript and committed together, so one event
signals both. Use
GET /v1/voicemails/{voicemail_id}
to retrieve them.
Both are best-effort, so the event can fire with only one of the two present.
It also fires when the transcript is too short to be worth summarizing, roughly
a sentence, with both summary and sentiment left null. Short voicemails
such as "Hi, call me back" are common, and for those the transcript itself is
the summary, so read the fields rather than assuming they are set.
When the AI step fails outright, no event is sent at all, so don't block a
voicemail's processing on this one — use voicemail.transcription.complete
for that.
Voicemail audio is single-channel, so its sentiment carries no local/remote
breakdown.
{
"id": "evt_01jqr5k8m3n4p6q7r8s9t0u1vc",
"type": "voicemail.summary.complete",
"created_at": "2026-01-15T14:36:20Z",
"account": "acct_01h2xcejqtf2nbrexx3vqjhp41",
"data": {
"voicemail_id": "vm_01h2xcejqtf2nbrexx3vqjhp46",
"user_id": "user_01h2xcejqtf2nbrexx3vqjhp42",
"call_id": "call_01h2xcejqtf2nbrexx3vqjhp45"
}
}
call_id identifies the call that produced the voicemail and is omitted when the call could not be resolved.
Fax events
The three fax events share a payload shape so a single fax.* handler can process all of them. fax.received is an inbound event; fax.delivered and fax.failed are outbound outcomes. They are terminal — a fax produces exactly one of fax.delivered or fax.failed, never both, and there are no intermediate queued/sending events.
Common fields:
| Field | Type | Description |
|---|---|---|
fax | string | Fax identifier. Use GET /v1/faxes/{fax_id} for full details |
from_number | string | Sending fax number (E.164) |
to_number | string | Receiving fax number (E.164) |
pages | number | null | Pages transmitted (see per-event notes) |
transport | string | null | Negotiated fax transport: "t38" (fax over IP) or "g711" (fax over audio) |
The transport value reflects how the fax was carried, not whether it succeeded — read fax.delivered vs fax.failed for the outcome. It is always present on fax.received; on the outbound events it is null when the call ended before a transport was negotiated.
fax.received
Sent when an inbound fax has been fully received and converted. pages and transport are always present.
{
"id": "evt_01jqr5k8m3n4p6q7r8s9t0u1vd",
"type": "fax.received",
"created_at": "2026-01-15T14:40:00Z",
"account": "acct_01h2xcejqtf2nbrexx3vqjhp41",
"data": {
"fax": "fax_01h2xcejqtf2nbrexx3vqjhp4a",
"from_number": "+14155551234",
"to_number": "+14155559876",
"pages": 3,
"transport": "t38"
}
}
fax.delivered
Sent when an outbound fax completed successfully — the receiving machine confirmed the full document. pages is the number of pages confirmed delivered.
{
"id": "evt_01jqr5k8m3n4p6q7r8s9t0u1ve",
"type": "fax.delivered",
"created_at": "2026-01-15T14:42:00Z",
"account": "acct_01h2xcejqtf2nbrexx3vqjhp41",
"data": {
"fax": "fax_01h2xcejqtf2nbrexx3vqjhp4b",
"from_number": "+14155559876",
"to_number": "+14155551234",
"pages": 2,
"transport": "t38"
}
}
fax.failed
Sent when an outbound fax did not complete. Carries an error_code in addition to the common fields.
{
"id": "evt_01jqr5k8m3n4p6q7r8s9t0u1vf",
"type": "fax.failed",
"created_at": "2026-01-15T14:44:00Z",
"account": "acct_01h2xcejqtf2nbrexx3vqjhp41",
"data": {
"fax": "fax_01h2xcejqtf2nbrexx3vqjhp4c",
"from_number": "+14155559876",
"to_number": "+14155551234",
"pages": 0,
"transport": "t38",
"error_code": "fax_far_end_unsupported_image_size"
}
}
| Field | Type | Description |
|---|---|---|
error_code | string | null | A short code for why the fax failed — e.g. dial_busy, dial_no_answer, fax_handshake_failed, fax_far_end_unsupported_image_size. After automatic retries, the code from the final attempt. The full list is on the Fax object |
pages | number | null | null when the page count is unknown (the fax never began transmitting); 0 when it connected but failed before the first page; N when N pages were sent before failing |
transport | string | null | null when the call failed before a transport was negotiated |
fax.delivered and fax.failed are mutually exclusive and final. A fax.delivered means the receiving machine acknowledged the whole document; a fax.failed means it did not, after all automatic retries.
Device provisioning events
device.provisioned, device.config_fetched and device.provisioning_failed report on a physical phone fetching its configuration from the provisioning server. They share a payload shape, so one handler can process all three. The device.hot_desk.* events below are not provisioning events and carry a different payload, so don't route on the device. prefix alone.
Pick the event that matches what you are reacting to:
device.provisionedfires each time a device leaves thepending-syncstate by successfully fetching its configuration — normally exactly once, at install. This is the "installation complete" signal — use it to kick off downstream provisioning workflows. A device you deliberately set back topending-sync(POST /v1/devices/:id) emits again on its next fetch, so treat a re-provision as a legitimate repeat rather than assuming one event per device forever.device.config_fetchedfires on every successful fetch. A deployed phone re-reads its configuration on each reboot and each check-sync, so expect this event repeatedly over a device's life, not just at install.device.provisioning_failedfires when a device was recognized but its configuration could not be generated.
Common fields:
| Field | Type | Description |
|---|---|---|
device | string | The deskphone or DECT base that fetched its configuration |
device_type | string | "deskphone" or "dect_base" |
mac_address | string | The device's MAC address |
vendor | string | Device vendor, e.g. "snom", "yealink", "polycom" |
model | string | null | Device model. null until the device reports a model we recognize — it is detected on first contact |
ip_address | string | null | Source IP of the configuration request. null when it could not be determined |
device.provisioned
{
"id": "evt_01jqr5k8m3n4p6q7r8s9t0u1w1",
"type": "device.provisioned",
"created_at": "2026-01-15T14:50:00Z",
"account": "acct_01h2xcejqtf2nbrexx3vqjhp41",
"data": {
"device": "dev_01h2xcejqtf2nbrexx3vqjhp4d",
"device_type": "deskphone",
"mac_address": "000413AABBCC",
"vendor": "snom",
"model": "D785",
"ip_address": "203.0.113.42",
"provisioned_at": "2026-01-15T14:50:00Z"
}
}
device.config_fetched
Same fields, with fetched_at in place of provisioned_at.
{
"id": "evt_01jqr5k8m3n4p6q7r8s9t0u1w2",
"type": "device.config_fetched",
"created_at": "2026-01-15T18:05:00Z",
"account": "acct_01h2xcejqtf2nbrexx3vqjhp41",
"data": {
"device": "dev_01h2xcejqtf2nbrexx3vqjhp4d",
"device_type": "deskphone",
"mac_address": "000413AABBCC",
"vendor": "snom",
"model": "D785",
"ip_address": "203.0.113.42",
"fetched_at": "2026-01-15T18:05:00Z"
}
}
device.provisioning_failed
Carries an error_code instead of model and ip_address.
{
"id": "evt_01jqr5k8m3n4p6q7r8s9t0u1w3",
"type": "device.provisioning_failed",
"created_at": "2026-01-15T14:52:00Z",
"account": "acct_01h2xcejqtf2nbrexx3vqjhp41",
"data": {
"device": "dev_01h2xcejqtf2nbrexx3vqjhp4d",
"device_type": "deskphone",
"mac_address": "000413AABBCC",
"vendor": "snom",
"error_code": "invalid_configuration",
"failed_at": "2026-01-15T14:52:00Z"
}
}
| Field | Type | Description |
|---|---|---|
error_code | string | Why configuration could not be generated — unsupported_vendor, invalid_configuration, or generation_failed |
A request that fails before the device is identified — an unknown provisioning URL, or an unrecognized device — produces no event, since it cannot be attributed to an account.
Hot desk events
The two device.hot_desk.* events follow hot-desk sessions: a user logged in on a phone that isn't theirs. Both carry the same device_hot_desk object as data.
device.hot_desk.startedfires when a user logs in on a phone, from the keypad, the admin portal or the API.device.hot_desk.endedfires when a session ends, whatever the cause: the user logged out, the session expired, the user logged in on another phone, another user logged in on this phone, the user or phone was deleted, or a handset was unpaired from its base. For an expiry, a deletion or an unpairing,endedcan arrive up to about a minute after the session actually ended.
A session emits started when it begins and ended when it ends. When a login replaces another user's session on the same phone, the old session's ended is emitted before the new session's started. Logging the same user in again on the same phone refreshes the session's expiry and emits nothing. As with every event, delivery is at least once: deduplicate on id. If you keep your own view of who is logged in where, reconcile it periodically from GET /v1/devices?expand[]=hot_desk rather than relying on the events alone.
| Field | Type | Description |
|---|---|---|
object | string | Always device_hot_desk |
device | string | The deskphone or DECT handset the user logged in on |
user | string | The user who logged in |
expires_at | string | null | When the session ends on its own. null when the account's sessions never expire |
device.hot_desk.started
{
"id": "evt_01jqr5k8m3n4p6q7r8s9t0u1w4",
"type": "device.hot_desk.started",
"created_at": "2026-10-19T14:00:00Z",
"account": "acct_01h2xcejqtf2nbrexx3vqjhp41",
"data": {
"object": "device_hot_desk",
"device": "dev_01h2xcejqtf2nbrexx3vqjhp4d",
"user": "user_01h2xcejqtf2nbrexx3vqjhp42",
"expires_at": "2026-10-19T22:00:00Z"
}
}
device.hot_desk.ended
Same fields, describing the session that ended.
{
"id": "evt_01jqr5k8m3n4p6q7r8s9t0u1w5",
"type": "device.hot_desk.ended",
"created_at": "2026-10-19T22:00:00Z",
"account": "acct_01h2xcejqtf2nbrexx3vqjhp41",
"data": {
"object": "device_hot_desk",
"device": "dev_01h2xcejqtf2nbrexx3vqjhp4d",
"user": "user_01h2xcejqtf2nbrexx3vqjhp42",
"expires_at": "2026-10-19T22:00:00Z"
}
}
port_order.updated
Sent when a port order changes status, or when its port date moves. Most of these changes come from the carrier and arrive within seconds of it reporting them, so you no longer need to poll the order to see it progress.
It fires when:
- the order changes
status, whether the change comes from the carrier (for examplesubmitted→foc→complete, or intoexception) or from your own request (approve, submit, resubmit, cancel).focmeans the port is scheduled: the carrier has committed to a date and time; - the carrier sets or moves the scheduled date,
actual_foc_date; - an
exceptionorder gets a newrejectioncode and stays inexception, for example when the carrier refuses a resubmitted order for a different reason; requested_foc_date,requested_foc_timeorrequested_foc_timezonechanges while the order issubmittedorfoc, which means it is live at the carrier.
It does not fire for creating an order, uploading documents, or editing a
draft or approved order that keeps its status. It also does not fire for
saving a correction to an exception order without resubmitting it: the carrier
has not seen that correction yet. When you resubmit, the event reports the status
change and the corrected date together.
{
"id": "evt_01h2xcejqtf2nbrexx3vqjhp4j",
"type": "port_order.updated",
"created_at": "2026-09-24T15:02:11Z",
"account": "acct_01h2xcejqtf2nbrexx3vqjhp41",
"data": {
"port_order": "port_01h2xcejqtf2nbrexx3vqjhp4k",
"status": "foc",
"phone_numbers": ["+14155550123", "+14155550124"],
"requested_foc_date": "2026-10-02",
"requested_foc_time": "09:00",
"actual_foc_date": "2026-10-02T13:00:00Z",
"previous_attributes": {
"status": "submitted",
"actual_foc_date": null
},
"changed_at": "2026-09-24T15:02:11Z"
}
}
| Field | Type | Description |
|---|---|---|
port_order | string | The port order that changed |
status | string | The order's status after the change: draft, approved, submitted, exception, foc, complete, or cancelled |
phone_numbers | array | The numbers on the order, in E.164 format |
requested_foc_date | string | The port date you asked for (YYYY-MM-DD), in requested_foc_timezone, when set |
requested_foc_time | string | The port time you asked for (HH:MM), in requested_foc_timezone, when set |
requested_foc_timezone | string | The IANA timezone of requested_foc_date and requested_foc_time. Only present when it was set on the order; when absent they are Eastern |
actual_foc_date | string | The scheduled port date and time the carrier committed to, once it has |
rejection | object | Only while status is exception: the code and message explaining what the carrier objected to. The codes are the same as the port order's details.rejection.code |
previous_attributes | object | The previous value of each of status, requested_foc_date, requested_foc_time, requested_foc_timezone, actual_foc_date, and rejection that changed in this update. A field that had no value before is null. A field that did not change is omitted |
request | string | Correlation ID for the API request that made the change. Omitted when the carrier made the change |
changed_at | string | When the order was updated with this change. Use it to order events for the same port order |
The payload is a summary. It leaves out the subscriber details, the PIN and
account number, the signature, and uploaded documents, because webhook bodies
often end up in logs and third-party tools. To read anything else, fetch the
order with GET /v1/port-orders/{id}.
Events for the same order can occasionally arrive out of order, or twice for
one change. Keep the one with the latest changed_at: it matches the order as
stored. If you need to be certain of the current state, fetch the order.
When a port completes, port_order.updated with status: "complete" is
delivered before the phone_number.activated event for each number it brought
into service.
Billable count changes
Four events report changes to the counts that drive your bill, as they happen, so you can reconcile before an invoice arrives rather than after:
user.created/user.deleted— a user became or stopped being billable.phone_number.activated/phone_number.disconnected— a number went into or out of service.
Each event names the resource that changed and carries the billable count either side of the change, so you can follow the running total without recounting:
{
"id": "evt_01h2xcejqtf2nbrexx3vqjhp4e",
"type": "user.created",
"created_at": "2026-01-15T14:50:00Z",
"account": "acct_01h2xcejqtf2nbrexx3vqjhp41",
"data": {
"user": "user_01h2xcejqtf2nbrexx3vqjhp4f",
"previous_billable_users": 11,
"current_billable_users": 12,
"request": "8f14e45fceea167a5a36dedd4bea2543",
"changed_at": "2026-01-15T14:50:00Z"
}
}
The phone_number.* events use the same shape with previous_billable_dids /
current_billable_dids, plus the number itself and the order it came from:
{
"id": "evt_01h2xcejqtf2nbrexx3vqjhp4g",
"type": "phone_number.activated",
"created_at": "2026-01-15T14:51:00Z",
"account": "acct_01h2xcejqtf2nbrexx3vqjhp41",
"data": {
"phone_number": "did_01h2xcejqtf2nbrexx3vqjhp4h",
"number": "+14155550123",
"order": "nord_01h2xcejqtf2nbrexx3vqjhp4i",
"previous_billable_dids": 3,
"current_billable_dids": 4,
"changed_at": "2026-01-15T14:51:00Z"
}
}
| Field | Type | Description |
|---|---|---|
user / phone_number | string | The resource whose billable state changed |
number | string | The phone number in E.164 format (phone_number.* only) |
order | string | The number order or port order this change came from, when there was one |
previous_billable_users / previous_billable_dids | integer | The billable count immediately before this change |
current_billable_users / current_billable_dids | integer | The billable count immediately after this change |
request | string | Correlation ID for the API request that made the change, when there was one — see below |
changed_at | string | When the change took effect |
These are resource counts, not invoice amounts
The counts tell you how many billable resources are in service, not what the next invoice will charge, so a change of +1 in these counts is not a change of one full unit on your bill. See How billing works for how a change during a billing period is charged. Use these events to reconcile what you have, and the invoice for what you owe.
A batch — one order activating several numbers — sends one event per number, each carrying its own step of the count, so the sequence reads as a running total.
Reassigning a number between two of your accounts sends two events: a
phone_number.disconnected on the account losing it and a
phone_number.activated on the account gaining it. Both carry the same
request, so you can pair them into one move from the payloads alone — but see
below for which of the two you can look that id up against.
What the running total does not cover
Every change that moves a billable count sends an event, with two exceptions:
- Deleting an account. Releasing its numbers and removing its users sends nothing — the account itself is gone, so there is nothing left to reconcile.
- Sandbox accounts. Sandbox users and numbers are never billed, so provisioning one asserts no billable change. See Testing your integration for how to trigger these events in sandbox on demand.
These are a signal, not a ledger
Read the count off current_* rather than accumulating your own tally: every
event carries the account's real count at the instant it was taken, so the most
recent event you have is the best number you have.
Do not use the previous_*/current_* chain as a dropped-delivery check. Two
changes committed at nearly the same moment each read the count after both have
landed, so they can legitimately report the same pair — a chain that lines up is
not proof you missed nothing, and a break in it is not proof you did. Delivery
is best-effort besides: the event is sent after the change is committed, so the
change can stand with no event behind it.
When the count has to be exact — reconciling an invoice, answering an audit —
recount from the API (GET /v1/users, GET /v1/phone-numbers) and use these
events for what they are: prompt notice that something moved.
Finding out who made the change
These payloads deliberately carry no actor. Instead, every change is attributable
through the audit log, which is a richer and permission-aware record. For
user.*, take the request value from the event and look it up:
curl "https://api.dialstack.ai/v1/audit-logs?request_id=8f14e45fceea167a5a36dedd4bea2543" \
-H "Authorization: Bearer sk_live_..."
The matching entry's actor identifies who performed the change. For a person,
resolve it to a name and email:
curl "https://api.dialstack.ai/v1/admin/users/{actor}" \
-H "Authorization: Bearer sk_live_..."
phone_number.* events split between the two join keys, and the reason is worth
knowing. A number usually goes into service when the upstream provider confirms
the order, not at the moment someone clicked, so there is no caller request to
correlate — and the audit entry for the number itself records the platform, not a
person. The human who requested the number is on the order's audit entry,
which is why those events carry order and no request. Look that up instead:
curl "https://api.dialstack.ai/v1/audit-logs?resource=nord_01h2xcejqtf2nbrexx3vqjhp4i" \
-H "Authorization: Bearer sk_live_..."
The other way round when the change came from a direct API call — activating or
deactivating a number, reassigning one, recovering a released one, or keeping a
temporary one. There is no order to point at, but there is a caller, so those
events carry request and no order. Look them up the same way as user.*,
with ?request_id=.
One exception on a reassignment: the audit entry is written under the account
whose key made the call — the account losing the number. So ?request_id=
resolves the actor against the source account, and returns nothing against
the target. On the target event, treat request as a correlation id that pairs
it with its source event, not as something you can look up on that account.
Related Resources
- Receiving Webhooks — endpoints, signature verification, retries, ordering
- Real-Time Events (SSE) — Frontend event streaming
- AI Scheduling — request/response scheduling webhooks
- Voice Apps — Programmable voice webhooks