Skip to content

PBX Webhook Events

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.

Common envelope

Every event has the same envelope:

FieldTypeDescription
eventstringEvent name (see below)
timestampdatetimeWhen Zorio published the event (ISO 8601 UTC)
dataobjectEvent-specific payload

pbx.call.ringing

A call has arrived at an extension and the extension is ringing (not yet answered).

data fields

FieldTypeDescriptionValues
call_uuidstringCall UUID
extensionstringThe extension that is ringing
directionstringCall directioninbound / outbound / internal
caller_numberstringOriginating number (caller ID)
destination_numberstringDestination number (usually equals extension)
timestampdatetimeWhen ringing startedISO 8601

Sample payload

json
{
  "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:

FieldTypeDescription
answered_atdatetimeWhen 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

FieldTypeDescriptionValid values
call_uuidstringCall UUID
extensionstringAssociated extension
directionstringCall directioninbound / outbound / internal
caller_numberstringCaller ID
destination_numberstringDestination number
started_atdatetime|nullWhen the call startedISO 8601
answered_atdatetime|nullWhen the call was answered (null if never answered)ISO 8601
ended_atdatetimeWhen the call endedISO 8601
duration_secintegerTotal duration (seconds)≥ 0
billsecintegerBillable time (seconds)≥ 0
hangup_causestringQ.850 hangup code from Zorio PBXNORMAL_CLEARING / NO_ANSWER / USER_BUSY / CALL_REJECTED / ORIGINATOR_CANCEL / ...
call_resultstringCoarse classificationanswered (billsec > 0) / no_answer (billsec = 0)
recording_urlstring|nullRecording download URL — null in the realtime event (sent immediately on hangup); populated in pbx.cdr.created after the recording pipeline finishes

Sample payload

json
{
  "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

FieldTypeDescriptionValid values
call_uuidstringCall UUID (= the call UUID)
extensionstring|nullExtension (extension_number preferred, otherwise agent_extension)
directionstringDirectioninbound / outbound / internal / local
caller_numberstringCaller ID
destination_numberstringDestination number
started_atdatetime|nullWhen the call startedISO 8601
answered_atdatetime|nullWhen the call was answeredISO 8601
ended_atdatetime|nullWhen the call endedISO 8601
duration_secintegerTotal duration≥ 0
billsecintegerBillable time≥ 0
hangup_causestringHangup code(same as pbx.call.hangup)
resultstringFull result classificationanswered / busy / no_answer / failed / cancelled / rejected / voicemail
recording_urlstring|nullInternal URL pointing to the download endpoint (requires Bearer Token)https://app.zorio.vn/api/pbx/cdr/{uuid}/recording or null

Differences vs 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.

Headers Zorio POSTs to your URL

HeaderMeaning
X-Zorio-Eventpbx.call.hangup
X-Zorio-TimestampEpoch seconds (UTC). The client should reject if skew is > 300s
X-Zorio-DeliveryPer-delivery UUID — use to dedupe on retry
X-Zorio-Signaturesha256=<hex> HMAC-SHA256(secret, raw_body)
Content-Typeapplication/json; charset=utf-8

Verifying HMAC on the client (Node.js sample)

js
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));
}

Retry policy

Zorio expects a 2xx response within 5 seconds. If not:

  • 2nd attempt: after 30 seconds
  • 3rd attempt: after 5 minutes
  • 4th attempt: after 30 minutes
  • Maximum 4 attempts → marked deadletter in the webhook delivery log

The client should return 200 immediately on receipt and process asynchronously — avoid stacking retries.

Cấp phép theo điều khoản sử dụng của Zorio.