English
English
Appearance
English
English
Appearance
Webhook registration
Webhook subscriptions are a per-customer configuration — register / delete / rotate the secret through the Admin UI (Admin Console). The public API does not expose subscription CRUD.
This page documents the payload schema + HMAC verification so the customer CRM can process events itself.
Every event has the same envelope:
| Field | Type | Description |
|---|---|---|
event | string | Event name (see below) |
timestamp | datetime | When Zorio published the event (ISO 8601 UTC) |
data | object | Event-specific payload |
pbx.call.ringing A call has arrived at an extension and the extension is ringing (not yet answered).
data fields | Field | Type | Description | Values |
|---|---|---|---|
call_uuid | string | Call UUID | |
extension | string | The extension that is ringing | |
direction | string | Call direction | inbound / outbound / internal |
caller_number | string | Originating number (caller ID) | |
destination_number | string | Destination number (usually equals extension) | |
timestamp | datetime | When ringing started | ISO 8601 |
{
"event": "pbx.call.ringing",
"timestamp": "2026-06-29T03:30:00Z",
"data": {
"call_uuid": "...",
"extension": "1001",
"direction": "inbound",
"caller_number": "0987654321",
"destination_number": "0900000020",
"timestamp": "2026-06-29T03:30:00Z"
}
}pbx.call.answered The extension has picked up.
data fields = same as pbx.call.ringing plus:
| Field | Type | Description |
|---|---|---|
answered_at | datetime | When the call was answered (ISO 8601) |
pbx.call.hangup The call has ended — fires immediately when Zorio PBX signals the end-of-call event.
data fields | Field | Type | Description | Valid values |
|---|---|---|---|
call_uuid | string | Call UUID | |
extension | string | Associated extension | |
direction | string | Call direction | inbound / outbound / internal |
caller_number | string | Caller ID | |
destination_number | string | Destination number | |
started_at | datetime|null | When the call started | ISO 8601 |
answered_at | datetime|null | When the call was answered (null if never answered) | ISO 8601 |
ended_at | datetime | When the call ended | ISO 8601 |
duration_sec | integer | Total duration (seconds) | ≥ 0 |
billsec | integer | Billable time (seconds) | ≥ 0 |
hangup_cause | string | Q.850 hangup code from Zorio PBX | NORMAL_CLEARING / NO_ANSWER / USER_BUSY / CALL_REJECTED / ORIGINATOR_CANCEL / ... |
call_result | string | Coarse classification | answered (billsec > 0) / no_answer (billsec = 0) |
recording_url | string|null | Recording download URL — null in the realtime event (sent immediately on hangup); populated in pbx.cdr.created after the recording pipeline finishes |
{
"event": "pbx.call.hangup",
"timestamp": "2026-06-29T03:30:45Z",
"data": {
"call_uuid": "...",
"extension": "1001",
"direction": "inbound",
"caller_number": "0987654321",
"destination_number": "0900000020",
"started_at": "2026-06-29T03:30:00Z",
"answered_at": "2026-06-29T03:30:03Z",
"ended_at": "2026-06-29T03:30:45Z",
"duration_sec": 45,
"billsec": 42,
"hangup_cause": "NORMAL_CLEARING",
"call_result": "answered",
"recording_url": null
}
}pbx.cdr.created Fires AFTER the CDR pipeline inserts the row into the database — contains all fields from the processed A/B legs plus recording_url.
data fields | Field | Type | Description | Valid values |
|---|---|---|---|
call_uuid | string | Call UUID (= the call UUID) | |
extension | string|null | Extension (extension_number preferred, otherwise agent_extension) | |
direction | string | Direction | inbound / outbound / internal / local |
caller_number | string | Caller ID | |
destination_number | string | Destination number | |
started_at | datetime|null | When the call started | ISO 8601 |
answered_at | datetime|null | When the call was answered | ISO 8601 |
ended_at | datetime|null | When the call ended | ISO 8601 |
duration_sec | integer | Total duration | ≥ 0 |
billsec | integer | Billable time | ≥ 0 |
hangup_cause | string | Hangup code | (same as pbx.call.hangup) |
result | string | Full result classification | answered / busy / no_answer / failed / cancelled / rejected / voicemail |
recording_url | string|null | Internal URL pointing to the download endpoint (requires Bearer Token) | https://app.zorio.vn/api/pbx/cdr/{uuid}/recording or null |
pbx.call.hangup pbx.call.hangup fires directly from the realtime event — result is only coarse (answered / no_answer).pbx.cdr.created fires after the CDR pipeline has fully processed the call → result is more detailed and includes recording_url.| Header | Meaning |
|---|---|
X-Zorio-Event | pbx.call.hangup |
X-Zorio-Timestamp | Epoch seconds (UTC). The client should reject if skew is > 300s |
X-Zorio-Delivery | Per-delivery UUID — use to dedupe on retry |
X-Zorio-Signature | sha256=<hex> HMAC-SHA256(secret, raw_body) |
Content-Type | application/json; charset=utf-8 |
const crypto = require('crypto');
function verify(req, secret) {
const ts = parseInt(req.headers['x-zorio-timestamp'], 10);
if (Math.abs(Date.now() / 1000 - ts) > 300) return false;
const sig = req.headers['x-zorio-signature'];
const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(req.rawBody).digest('hex');
return crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
}Zorio expects a 2xx response within 5 seconds. If not:
deadletter in the webhook delivery logThe client should return 200 immediately on receipt and process asynchronously — avoid stacking retries.