Webhooks
Webhooks notify your server in real time when something happens to a call on your phone system, such as when it starts ringing, is answered, or ends. Use them instead of polling to react to calls as they happen.
{YOUR_WEBHOOK_URL}Setup
Webhooks are enabled by request. There's no self-service webhook setup. Contact our team and we'll configure webhooks for your domain once your request is approved.
- Send us the HTTPS URL where you want to receive notifications.
- Tell us which event types you want. You can enable any combination.
- Once approved, we enable delivery to your URL and send you your signing secret and verification details.
- Optionally, ask us for a
call.testdelivery to confirm your endpoint receives and verifies requests.
Event Types
| Event | Type value | Sent when |
|---|---|---|
| Call Ringing | call.ringing | A call starts ringing at a destination, such as an extension. |
| Call Answered | call.answered | A call is answered. |
| Call Ended | call.ended | A call ends. |
| Test | call.test | We send a test delivery to check your endpoint. |
One call can send several events. When a call rings more than one extension at once, such as through a ring group, each leg sends its own events with its own interaction_id. Legs that don't answer end with call.ended too. For example, a leg that lost to another extension that answered first reports "hangup_cause": "LOSE_RACE".
Request Headers
Each webhook is an HTTPS request with a JSON body and these headers:
| Header | Description |
|---|---|
content-type | Always application/json. |
user-agent | ZVMS-Call-Webhooks/1.0 |
x-zvms-event-type | The event type, for example call.ended. Matches the body's type field. |
x-zvms-event-id | Unique ID of this event. Use it to detect duplicate deliveries. |
timestamp | When the request was sent, as a Unix epoch timestamp in seconds. |
signature | HMAC-SHA256 signature of the request, generated with your signing secret. See Verifying Requests. |
content-type: application/json user-agent: ZVMS-Call-Webhooks/1.0 x-zvms-event-type: call.ended x-zvms-event-id: 28f5cb99-dfa3-4490-8454-8290ce9f24e3 timestamp: 1793346975 signature: d4d139ce1cd3a94174008a5614167ee7b1f415b0a9db036c85d25f21247f6751
Payload
Every event uses the same body structure. The state object fills in as the call progresses.
| Field | Type | Description |
|---|---|---|
id | string (UUID) | Unique ID of the event. Same value as the x-zvms-event-id header. |
type | string | The event type, for example call.ringing. |
occurred_at | string | When the event happened, as an ISO 8601 timestamp in UTC. |
data.interaction_id | string (UUID) | ID of the call leg the event is about. Events for the same leg share this ID. |
data.channel_uuid | string (UUID) | The call leg's channel ID. |
data.domain_uuid | string (UUID) | Your domain's UUID. |
data.direction | string | Call direction, for example inbound. |
data.caller.name | string | Caller ID name. URL-encoded. |
data.caller.number | string | Caller ID number. URL-encoded, so + arrives as %2B. |
data.destination_number | string | The number or extension the call was sent to. |
data.target | object | Where the call leg was routed. See Target fields. |
data.state.answered_at | string or null | When the leg was answered (ISO 8601, UTC), or null. |
data.state.ended_at | string or null | When the leg ended (ISO 8601, UTC), or null. |
data.state.outcome | string or null | Outcome of the leg, set on call.ended, for example failed. |
data.state.hangup_cause | string or null | Telephony hangup cause, set on call.ended, for example LOSE_RACE. |
Decode caller ID values. caller.name and caller.number are URL-encoded. For example, %2B15555550123 decodes to +15555550123. Decode them before displaying or matching them.
Target fields
| Field | Description |
|---|---|
type | The kind of destination, for example extension. |
extension_uuid, extension | The extension's ID and number. |
call_center_agent_uuid, agent_id, agent_name | The call center agent, when the call was routed to one. Otherwise null. |
call_center_queue_uuid, queue_extension, queue_name | The call center queue, when the call came through one. Otherwise null. |
Example: call.ended
{
"id": "28f5cb99-dfa3-4490-8454-8290ce9f24e3",
"type": "call.ended",
"occurred_at": "2026-10-28T17:59:56.802Z",
"data": {
"interaction_id": "4a4d5e6e-6f75-4c70-8310-84b4e9bd678c",
"channel_uuid": "4a4d5e6e-6f75-4c70-8310-84b4e9bd678c",
"domain_uuid": "YOUR_DOMAIN_UUID",
"direction": "inbound",
"caller": {
"name": "JANE%20DOE",
"number": "%2B15555550123"
},
"destination_number": "110",
"target": {
"type": "extension",
"extension_uuid": "4cf62e35-75e6-4711-a644-12e07650f602",
"extension": "110",
"call_center_agent_uuid": null,
"agent_id": null,
"agent_name": null,
"call_center_queue_uuid": null,
"queue_extension": null,
"queue_name": null
},
"state": {
"answered_at": null,
"ended_at": "2026-10-28T17:59:56.802Z",
"outcome": "failed",
"hangup_cause": "LOSE_RACE"
}
}
}
call.ringing and call.answered use the same structure. On call.ringing, every state field is null. On call.answered, state.answered_at is set.
Call analysis isn't a webhook event. There's no notification when a call's Call Analysis is ready. After call.ended, request the analysis and retry with backoff while it's still processing, as described in Processing Behavior.
Verifying Requests
Every request is signed so you can confirm it came from ZVMS Telecom and wasn't altered. The signature header holds an HMAC-SHA256 signature generated with your signing secret, and the timestamp header records when the request was sent.
- We send your signing secret and verification details when your webhook request is approved.
- Verify against the request exactly as received, before any JSON parsing or reformatting.
- Compare signatures with a constant-time comparison, and reject requests whose signature doesn't match.
- Reject requests whose
timestampis too far from your server's current time, to limit replayed requests.
Keep your signing secret private. Store it on your server only, never in client-side code or public repositories. If you think it has been exposed, contact us right away so we can issue a new one.
Best Practices
- Respond quickly. Return a
2xxstatus as soon as you've verified the request, and do any slow processing afterwards. - Handle duplicates. Use
x-zvms-event-idto skip events you've already processed. - Don't rely on order. Use
occurred_atto order events instead of the order they arrive in. - Treat payloads as private data. Webhooks carry caller names and numbers, so restrict access to them within your application.