Call Events
Call events track the full real-time lifecycle of phone calls on PBXware. Understanding a few core concepts makes it straightforward to build accurate, resilient integrations.
The Call Leg Model
PBXware represents calls as a collection of legs. A call leg represents one participant's side of the call — their state, their direction, their events. A single leg may abstract multiple physical channels: for example, if an extension rings simultaneously on a softphone and a deskphone, PBXware still exposes a single leg for that extension.
Every participant in a call has their own leg. A simple two-party call produces two legs. A call that is transferred produces a third leg when the new party joins. Conference calls produce a leg per participant.
Each leg generates its own independent stream of events. You correlate legs using a shared linked_id.
Call Leg Identifiers
| Field | Scope | Description |
|---|---|---|
uid | Per-leg | Unique ID for this specific call leg |
linked_id | Per-call | Shared by all legs of the same call; always equals the UID of the first leg spawned for that call |
Three fields together tell you who a leg belongs to and their role in the call:
| Field | Meaning |
|---|---|
is_pbx_user | true — this leg belongs to a local PBXware extension; false — external party (PSTN/SIP trunk) |
number | The phone number or extension identifier of this leg's participant |
is_incoming | true — this leg is receiving the call (callee); false — this leg is placing the call (caller) |
Key rule: Use
linked_idto group all events belonging to the same call across multiple legs.
Call Leg Lifecycle
Every call leg follows this sequence:
event_call_started
│
├─(0 or more)─→ event_call_updated
│
├─────────────→ event_call_connected
│ │
│ (0 or more) → event_call_updated
│ │
│ └──→ event_call_finished
│
└─────────────→ event_call_finished (if never answered)
| Event | When it fires |
|---|---|
event_call_started | Leg created — participant ringing or dial initiated |
event_call_updated | Any field on the leg changes (see below for common triggers) |
event_call_connected | Leg answered; media is flowing |
event_call_finished | Leg ended |
event_call_updated may fire zero or more times at any stage.
Event Reference
event_call_started · Full spec
Fires when a call leg is created. The leg is not yet connected — it is either dialing out (RingOut) or waiting to be answered (RingIn).
call_type is always 1on1Call at creation — even conference calls begin as one-on-one. The type only changes to Conference later via event_call_updated when the leg joins a conference bridge.
event_call_updated · Full spec
Fires whenever any field on the leg changes. Common triggers include:
- Caller ID lookup resolves —
connected_namepopulates after a lookup completes - Transfer —
connected_numandconnected_namechange to reflect the new party - Recording state change —
record_statuschanges - Conference join/leave —
call_typechanges toConference,confidpopulates - Queue transitions —
queue_agent_infoorqueue_caller_infoupdates
Subscribe to event_call_updated when you need to track mid-call changes like recording state or transfers.
event_call_connected · Full spec
Fires when the leg answers.
event_call_finished · Full spec
Fires when the call leg ends. duration is 0 if the leg was never answered.
call_rating · Full spec
Fires when a call has been rated against the applicable service plan. Rating applies rules such as minimum charges, connection charges, grace periods, and rating increments to produce a cost and a rated duration. The call_rating_duration may therefore differ from the actual call duration.
This is not a call leg event — there is no uid. Use linked_id to correlate it with the call.
customer_interaction · Full spec
Provides an aggregated summary of the interaction between an external party and the extension(s) that handled their call. This is the primary event for CRM integrations — it gives you a clean, per-segment record without needing to reconstruct the interaction from individual leg events.
A transferred call produces one customer_interaction per extension involved, all sharing the same linked_id. If no extension answered, a single event is still emitted reflecting the unanswered interaction.
voicemail_received · Full spec
Fires when a voicemail is captured.
ID Correlation
To reconstruct a complete call from a stream of events:
- Group by
linked_id— all events for the same call share this value - Separate by
uid— each uniqueuidwithin the group is a different leg
Events belonging to the same
linked_idare always delivered sequentially — no ordering step is needed. Events from different calls may interleave, which is why grouping bylinked_idfirst is essential.
After a transfer, the new leg gets a new uid but the same linked_id — it is still the same call. The transferring leg fires event_call_finished while the trunk leg fires event_call_updated to reflect the new party.
Detecting when a call is fully over
A call is completely finished when every leg that started has also finished — that is, the count of event_call_finished events for a linked_id equals the count of event_call_started events for the same linked_id. This works regardless of how many transfers or participants were involved.
Example: Inbound Call with Transfer
An external caller reaches extension 170, which transfers to extension 171. All 14 events share linked_id: "1778836418.145".
+0s +5s +12s +21s +36s
TRUNK ●────────────●───────────●────────────────────────────●
started connected updated finished
RingOut transfer → 171 dur=31s
EXT 170 ●────────────●───────────●
started connected finished
RingIn dur=7s
EXT 171 ●────────────●───────────────●
started connected finished
RingIn dur=15s
● call_rating
● customer_interaction ×2
| # | Event | uid | number | Notes |
|---|---|---|---|---|
| 1 | event_call_started | 1778836418.145 | +15550143200 | External caller ringing in; state=RingOut, is_pbx_inbound_call=true, is_pbx_user=false |
| 2 | event_call_started | 1778836418.145.200170 | 170 | Extension 170 ringing; state=RingIn, is_pbx_user=true |
| 3 | event_call_updated | 1778836418.145 | +15550143200 | connected_name resolves to "Alice Green" (caller ID lookup) |
| 4 | event_call_connected | 1778836418.145.200170 | 170 | Extension 170 answers; state=Connected |
| 5 | event_call_connected | 1778836418.145 | +15550143200 | External leg reflects connected state |
| 6 | event_call_finished | 1778836418.145.200170 | 170 | Extension 170 transfers; duration=7 |
| 7 | event_call_updated | 1778836418.145 | +15550143200 | Transfer: connected_num=171, connected_name="Carol White" |
| 8 | event_call_started | 1778836418.145.200171 | 171 | New leg for extension 171; state=RingIn |
| 9 | event_call_connected | 1778836418.145.200171 | 171 | Extension 171 answers |
| 10 | event_call_finished | 1778836418.145.200171 | 171 | Extension 171 hangs up; duration=15 |
| 11 | event_call_finished | 1778836418.145 | +15550143200 | Trunk leg ends; duration=31 |
| 12 | call_rating | — | — | call_rating_duration=31, call_cost=0.0 |
| 13 | customer_interaction | — | — | extension=170, duration=7 |
| 14 | customer_interaction | — | — | extension=171, duration=14 |
Raw event payloads
{
"event": "event_call_started",
"event_id": "a298608c-16a2-4fdb-b2f5-29415e07feee",
"spec_version": "1.0",
"tenant_code": "200",
"event_time_info": { "timestamp": 1778836418, "date": "2026-05-15", "time": "11:13:38" },
"payload": {
"uid": "1778836418.145",
"linked_id": "1778836418.145",
"number": "+15550143200",
"connected_num": "+18005550100",
"connected_name": "",
"state": "RingOut",
"call_type": "1on1Call",
"is_pbx_inbound_call": true,
"is_pbx_user": false,
"is_incoming": false,
"did": "+18005550100",
"trunk": "192.168.203.46",
"call_started_timestamp": 1778836418,
"call_started_datetime": "2026-05-15 11:13:38",
"record_status": "NotRecording"
}
}
{
"event": "event_call_started",
"event_id": "a2f47385-4ada-485b-aa68-b2b4f940eaad",
"spec_version": "1.0",
"tenant_code": "200",
"event_time_info": { "timestamp": 1778836418, "date": "2026-05-15", "time": "11:13:38" },
"payload": {
"uid": "1778836418.145.200170",
"linked_id": "1778836418.145",
"number": "170",
"connected_num": "+15550143200",
"connected_name": "",
"state": "RingIn",
"call_type": "1on1Call",
"is_pbx_inbound_call": false,
"is_pbx_user": true,
"is_incoming": true,
"did": "+18005550100",
"call_started_timestamp": 1778836418,
"call_started_datetime": "2026-05-15 11:13:38",
"record_status": "NotRecording"
}
}
{
"event": "event_call_updated",
"event_id": "a58f46af-9927-401b-bd1f-991dc5c32b4a",
"spec_version": "1.0",
"tenant_code": "200",
"event_time_info": { "timestamp": 1778836418, "date": "2026-05-15", "time": "11:13:38" },
"payload": {
"uid": "1778836418.145",
"linked_id": "1778836418.145",
"number": "+15550143200",
"connected_num": "170",
"connected_name": "Alice Green",
"state": "RingOut",
"call_type": "1on1Call",
"is_pbx_inbound_call": true,
"is_pbx_user": false,
"is_incoming": false,
"did": "+18005550100",
"trunk": "192.168.203.46",
"call_started_timestamp": 1778836418,
"call_started_datetime": "2026-05-15 11:13:38",
"record_status": "NotRecording"
}
}
{
"event": "event_call_connected",
"event_id": "b25842f3-fdc6-4e06-9bdd-3abb3cb9b930",
"spec_version": "1.0",
"tenant_code": "200",
"event_time_info": { "timestamp": 1778836423, "date": "2026-05-15", "time": "11:13:43" },
"payload": {
"uid": "1778836418.145.200170",
"linked_id": "1778836418.145",
"number": "170",
"connected_num": "+15550143200",
"connected_name": "",
"state": "Connected",
"call_type": "1on1Call",
"is_pbx_inbound_call": false,
"is_pbx_user": true,
"is_incoming": true,
"did": "+18005550100",
"call_started_timestamp": 1778836418,
"call_started_datetime": "2026-05-15 11:13:38",
"record_status": "NotRecording"
}
}
{
"event": "event_call_connected",
"event_id": "4bd80ca2-3b6f-45c6-b7cf-966990bd6f0f",
"spec_version": "1.0",
"tenant_code": "200",
"event_time_info": { "timestamp": 1778836423, "date": "2026-05-15", "time": "11:13:43" },
"payload": {
"uid": "1778836418.145",
"linked_id": "1778836418.145",
"number": "+15550143200",
"connected_num": "170",
"connected_name": "Alice Green",
"state": "Connected",
"call_type": "1on1Call",
"is_pbx_inbound_call": true,
"is_pbx_user": false,
"is_incoming": false,
"did": "+18005550100",
"trunk": "192.168.203.46",
"call_started_timestamp": 1778836418,
"call_started_datetime": "2026-05-15 11:13:38",
"record_status": "NotRecording"
}
}
{
"event": "event_call_finished",
"event_id": "6bed04a6-8b98-4889-a0e4-313cfb79eef6",
"spec_version": "1.0",
"tenant_code": "200",
"event_time_info": { "timestamp": 1778836430, "date": "2026-05-15", "time": "11:13:50" },
"payload": {
"uid": "1778836418.145.200170",
"linked_id": "1778836418.145",
"number": "170",
"connected_num": "+15550143200",
"connected_name": "",
"state": "Connected",
"call_type": "1on1Call",
"is_pbx_inbound_call": false,
"is_pbx_user": true,
"is_incoming": true,
"did": "+18005550100",
"call_started_timestamp": 1778836418,
"call_started_datetime": "2026-05-15 11:13:38",
"record_status": "NotRecording",
"duration": 7,
"is_answered_elsewhere": false
}
}
{
"event": "event_call_updated",
"event_id": "022a8869-0f81-4dd4-994b-96fde526f600",
"spec_version": "1.0",
"tenant_code": "200",
"event_time_info": { "timestamp": 1778836430, "date": "2026-05-15", "time": "11:13:50" },
"payload": {
"uid": "1778836418.145",
"linked_id": "1778836418.145",
"number": "+15550143200",
"connected_num": "171",
"connected_name": "Carol White",
"state": "Connected",
"call_type": "1on1Call",
"is_pbx_inbound_call": true,
"is_pbx_user": false,
"is_incoming": false,
"did": "+18005550100",
"trunk": "192.168.203.46",
"call_started_timestamp": 1778836418,
"call_started_datetime": "2026-05-15 11:13:38",
"record_status": "NotRecording"
}
}
{
"event": "event_call_started",
"event_id": "f4602f23-8912-49ce-b03b-166bdd3dbf5c",
"spec_version": "1.0",
"tenant_code": "200",
"event_time_info": { "timestamp": 1778836430, "date": "2026-05-15", "time": "11:13:50" },
"payload": {
"uid": "1778836418.145.200171",
"linked_id": "1778836418.145",
"number": "171",
"connected_num": "+15550143200",
"connected_name": "",
"state": "RingIn",
"call_type": "1on1Call",
"is_pbx_inbound_call": false,
"is_pbx_user": true,
"is_incoming": true,
"did": "+18005550100",
"call_started_timestamp": 1778836430,
"call_started_datetime": "2026-05-15 11:13:50",
"record_status": "NotRecording"
}
}
{
"event": "event_call_connected",
"event_id": "b2e3d314-5e12-49dd-bd17-9dfd3fabdcff",
"spec_version": "1.0",
"tenant_code": "200",
"event_time_info": { "timestamp": 1778836439, "date": "2026-05-15", "time": "11:13:59" },
"payload": {
"uid": "1778836418.145.200171",
"linked_id": "1778836418.145",
"number": "171",
"connected_num": "+15550143200",
"connected_name": "",
"state": "Connected",
"call_type": "1on1Call",
"is_pbx_inbound_call": false,
"is_pbx_user": true,
"is_incoming": true,
"did": "+18005550100",
"call_started_timestamp": 1778836430,
"call_started_datetime": "2026-05-15 11:13:50",
"record_status": "NotRecording"
}
}
{
"event": "event_call_finished",
"event_id": "bccabb31-5988-4654-bcb3-bc1c35eb0b18",
"spec_version": "1.0",
"tenant_code": "200",
"event_time_info": { "timestamp": 1778836454, "date": "2026-05-15", "time": "11:14:14" },
"payload": {
"uid": "1778836418.145.200171",
"linked_id": "1778836418.145",
"number": "171",
"connected_num": "+15550143200",
"connected_name": "",
"state": "Connected",
"call_type": "1on1Call",
"is_pbx_inbound_call": false,
"is_pbx_user": true,
"is_incoming": true,
"did": "+18005550100",
"call_started_timestamp": 1778836430,
"call_started_datetime": "2026-05-15 11:13:50",
"record_status": "NotRecording",
"duration": 15,
"is_answered_elsewhere": false
}
}
{
"event": "event_call_finished",
"event_id": "ca61cb88-ad28-45a1-bd6e-561f17ce2b7a",
"spec_version": "1.0",
"tenant_code": "200",
"event_time_info": { "timestamp": 1778836454, "date": "2026-05-15", "time": "11:14:14" },
"payload": {
"uid": "1778836418.145",
"linked_id": "1778836418.145",
"number": "+15550143200",
"connected_num": "171",
"connected_name": "Carol White",
"state": "Connected",
"call_type": "1on1Call",
"is_pbx_inbound_call": true,
"is_pbx_user": false,
"is_incoming": false,
"did": "+18005550100",
"trunk": "192.168.203.46",
"call_started_timestamp": 1778836418,
"call_started_datetime": "2026-05-15 11:13:38",
"record_status": "NotRecording",
"duration": 31,
"is_answered_elsewhere": false
}
}
{
"event": "call_rating",
"event_id": "5db6812c-360f-46a3-97a5-a0efb6332208",
"spec_version": "1.0",
"tenant_code": "200",
"event_time_info": { "timestamp": 1778836454, "date": "2026-05-15", "time": "11:14:14" },
"payload": {
"linked_id": "1778836418.145",
"call_cost": 0.0,
"call_rating_duration": 31
}
}
{
"event": "customer_interaction",
"event_id": "c1475f78-bf99-48bf-b6b6-dd244ecabfab",
"spec_version": "1.0",
"tenant_code": "200",
"event_time_info": { "timestamp": 1778836454, "date": "2026-05-15", "time": "11:14:14" },
"payload": {
"linked_id": "1778836418.145",
"customer_number": "+15550143200",
"extension": "170",
"duration": 7,
"direction": "Inbound",
"is_answered": true,
"call_start_time": 1778836418,
"subject": "",
"description": ""
}
}
{
"event": "customer_interaction",
"event_id": "7e8fd612-9bc0-4480-893f-e7fe44ef654d",
"spec_version": "1.0",
"tenant_code": "200",
"event_time_info": { "timestamp": 1778836454, "date": "2026-05-15", "time": "11:14:14" },
"payload": {
"linked_id": "1778836418.145",
"customer_number": "+15550143200",
"extension": "171",
"duration": 14,
"direction": "Inbound",
"is_answered": true,
"call_start_time": 1778836430,
"subject": "",
"description": ""
}
}
Integration Examples
The examples below illustrate a few common patterns. The event payloads carry rich context — caller identity, queue state, recording status — so the range of integrations you can build is broad.
CRM Call Logging
Use customer_interaction events for the most straightforward CRM integration. Each event represents one extension's segment of a conversation with a customer.
app.post('/webhook', (req, res) => {
res.sendStatus(200); // acknowledge immediately
const { event, payload, event_id } = req.body;
if (event !== 'customer_interaction') return;
crm.logCall({
idempotencyKey: event_id, // prevent duplicate logging on retries
phoneNumber: payload.customer_number,
agent: payload.extension,
duration: payload.duration, // seconds this extension was on the call
direction: payload.direction, // "Inbound" or "Outbound"
answered: payload.is_answered,
callId: payload.linked_id, // join key for the full call
startedAt: payload.call_start_time,
});
});
For transferred calls, you receive one
customer_interactionper extension with its ownduration. Both share the samelinked_id, so you can link them to a single call record.
Inbound External Call Tracker
Track all active calls arriving through trunks by following the trunk leg only. When an external caller reaches a DID, the trunk leg fires event_call_started with is_pbx_inbound_call=true. Its connected_num and connected_name reflect which extension currently handles the call — these update automatically on transfer via event_call_updated.
const inboundCalls = new Map(); // uid → call record
app.post('/webhook', (req, res) => {
res.sendStatus(200);
const { event, payload, tenant_code } = req.body;
if (event === 'event_call_started' && payload.is_pbx_inbound_call) {
inboundCalls.set(payload.uid, {
caller: payload.number,
did: payload.did,
connectedTo: payload.connected_num,
connectedName: payload.connected_name,
isAnswered: false,
isRecording: payload.record_status === 'RecordingActive',
tenant: tenant_code,
});
}
if (event === 'event_call_updated' && inboundCalls.has(payload.uid)) {
const call = inboundCalls.get(payload.uid);
call.connectedTo = payload.connected_num;
call.connectedName = payload.connected_name;
call.isRecording = payload.record_status === 'RecordingActive';
}
if (event === 'event_call_connected' && inboundCalls.has(payload.uid)) {
inboundCalls.get(payload.uid).isAnswered = true;
}
if (event === 'event_call_finished' && inboundCalls.has(payload.uid)) {
inboundCalls.delete(payload.uid);
}
});
Billing and Cost Tracking
call_rating fires when a call has been rated.
if (event === 'call_rating') {
billing.record({
callId: payload.linked_id,
cost: payload.call_cost,
ratedDuration: payload.call_rating_duration,
tenant: tenant_code,
});
}
Abandoned Queue Call Alert
When a caller hangs up while waiting in a queue, the caller leg's event_call_finished arrives with queue_caller_info.reason === 'abandoned'. Agent legs that rang for the same call carry queue_agent_info — collect them to tell the supervisor which agents were rung.
const queueAgents = new Map(); // linked_id → [agent names]
app.post('/webhook', (req, res) => {
res.sendStatus(200);
const { event, payload } = req.body;
if (event === 'event_call_started' && payload.queue_agent_info) {
if (!queueAgents.has(payload.linked_id)) {
queueAgents.set(payload.linked_id, []);
}
queueAgents.get(payload.linked_id).push({
name: payload.queue_agent_info.agent_name,
ext: payload.number,
});
}
if (event === 'event_call_finished' && payload.queue_caller_info) {
if (payload.queue_caller_info.reason === 'abandoned') {
const info = payload.queue_caller_info;
const agents = queueAgents.get(payload.linked_id) || [];
email.send({
to: 'supervisor@company.com',
subject: `Abandoned call in ${info.queue}`,
body: `Caller ${payload.number} waited ${info.wait_time}s in ${info.queue} and hung up.\n` +
`Agents rung: ${agents.map(a => `${a.name} (${a.ext})`).join(', ') || 'none'}`,
});
}
queueAgents.delete(payload.linked_id);
}
});
Voicemail Notifications
if (event === 'voicemail_received') {
notify.send({
to: payload.to_email,
subject: `New voicemail from ${payload.from_number}`,
body: `A voicemail was left for extension ${payload.to_number}.`,
});
}