Concept
Webhooks & callbacks
TelVox reaches your app in three ways. The webhook asks what to do with a live call, and its action URLs ask again after Gather, Dial and Record. Status callbacks report each step of a call. Recording status callbacks report each step of a recording. All of them are HTTP requests with JSON bodies.
Connect is live. Callback field names follow Twilio's where one exists. The API is v0.x, so field names, especially the extra TelVox ones, may change before v1.
Webhook vs status callback
These look alike but do different jobs. The webhook is synchronous and instructive: TelVox waits for your reply and runs the call-control markup you return. A status callback is informational: TelVox tells you the call changed state and only needs a quick 2xx. The call carries on whatever you return.
| Webhook and action URLs | Status callbacks | |
|---|---|---|
| Where set | Inbound: the number's webhook URL in the dashboard. Outbound: url on POST /api/calls. Action URLs: the action attribute in your markup. | Outbound: statusCallback on POST /api/calls. Inbound: the number's status callback URL in the dashboard. |
| Your reply | XML or JSON markup. | Any 2xx. The body is ignored. |
| Effect | Drives the live call. | Reports state. Doesn't steer. |
Inbound webhooks and action URLs time out after 5 seconds. If the inbound webhook fails, the caller hears a short message and the call ends. The webhook bodies and action URL bodies are listed on the call-control markup page.
Status callbacks
Pass statusCallback when you create a call (for inbound calls, set it on the number in the dashboard). TelVox POSTs JSON there as the call moves on. You always get ringing, in-progress and one final status. initiated comes only if statusCallbackEvent includes it.
| CallStatus | Meaning |
|---|---|
| initiated | The call is being placed. Sent only if statusCallbackEvent includes initiated. |
| ringing | The destination is ringing. |
| in-progress | The call was answered. |
| completed | Final. The call was answered and has ended. |
| busy | Final. The destination was busy. |
| no-answer | Final. Nobody answered. |
| failed | Final. The call could not be connected. |
| canceled | Final. The call was ended before it was answered. |
The body uses Twilio's CallProgressEvents field names (CallSid, CallStatus, From, To, Direction, SequenceNumber, Timestamp) plus a few TelVox fields such as WebhookEvent, CallbackId and IsFinal. The final callback has WebhookEvent set to call.hangup, IsFinal set to true, and adds Duration (whole minutes, rounded up) and CallDuration (seconds), both as strings, with HangupSource, HangupReason and HangupCause when known. Fields with no value are left out.
POST /voice/status HTTP/1.1
Host: your.app
Content-Type: application/json
User-Agent: cPaaS-Platform/1.0
X-CPaaS-Event: call.status
X-CPaaS-Call-Sid: CA7Q2M9X4K1B8N3T6R0W5Y2Z9C1V4H7JL5
{
"ApiVersion": "2010-04-01",
"AccountSid": "AC3b1f...",
"CallSid": "CA7Q2M9X4K1B8N3T6R0W5Y2Z9C1V4H7JL5",
"CallStatus": "in-progress",
"Direction": "outbound-api",
"From": "+918045670100",
"To": "+919876500199",
"Caller": "+918045670100",
"Called": "+919876500199",
"CallerCountry": "IN",
"FromCountry": "IN",
"CallbackSource": "call-progress-events",
"SequenceNumber": "2",
"Timestamp": "Mon, 05 Oct 2026 10:15:04 +0000",
"EventTimestamp": "2026-10-05T10:15:04.000Z",
"WebhookEvent": "call.status",
"EventType": "call.status",
"CallbackId": "CA7Q2M9X4K1B8N3T6R0W5Y2Z9C1V4H7JL5:2",
"IsFinal": false,
"WebhookUrl": "https://your.app/voice/answer"
}HTTP/1.1 200 OK
# Any 2xx, within 5 seconds. The body is ignored.
# Do slow work (database writes, other APIs) on a queue.Recording status callbacks
A separate callback reports each step of a recording, so you know when a GET on the RecordingUrl (/api/recordings/{name}, with your API key) will return the file. Set it with the recordingStatusCallback attribute on <Dial>, <Record> or <Conference> in your markup. By default only completed is sent. List more in recordingStatusCallbackEvent, and use recordingStatusCallbackMethod to switch to GET.
| RecordingStatus | Meaning |
|---|---|
| in-progress | Recording has started. |
| completed | Recording finished. The WAV file is stored and can be downloaded. |
| absent | No recording was produced, for example because nothing was captured. |
| failed | Recording could not be completed. |
POST /voice/rec-status HTTP/1.1
Host: your.app
Content-Type: application/json
User-Agent: cPaaS-Platform/1.0
X-CPaaS-Event: recording.status.completed
X-CPaaS-Call-Sid: CA7Q2M9X4K1B8N3T6R0W5Y2Z9C1V4H7JL5
{
"RecordingStatus": "completed",
"RecordingName": "cpaas-channel-CA7Q2M9X4K1B8N3T6R0W5Y2Z9C1V4H7JL5-1759659351000",
"RecordingUrl": "https://connect.telvox.dev/api/recordings/cpaas-channel-CA7Q2M9X4K1B8N3T6R0W5Y2Z9C1V4H7JL5-1759659351000",
"RecordingDuration": 47,
"RecordingCompletedAt": "2026-10-05T10:15:51.000Z",
"CallSid": "CA7Q2M9X4K1B8N3T6R0W5Y2Z9C1V4H7JL5"
}Securing your endpoint
Requests from TelVox are not signed. To be sure a request came from TelVox and belongs to you:
- Use HTTPS, and put a long random token in the callback URL (in the path or the query string). Check it on every request and reject anything without it.
- Check
X-CPaaS-Call-Sidagainst the calls you placed, or the calls to numbers you own. - Before acting on anything important, confirm it with
GET /api/calls/{callSid}.
Every request carries User-Agent: cPaaS-Platform/1.0, X-CPaaS-Event (for example call.status, call.hangup or recording.status.completed) and X-CPaaS-Call-Sid. These help you route and log requests, but anyone can set them, so they are not proof on their own.
Delivery
Each callback is sent once, with a 5 second timeout. It is not retried. If your endpoint is down or slow, that callback is lost.
- Reply 2xx fast and do slow work on a queue, so the handler never times out.
- De-duplicate on
CallbackId(CallSid:SequenceNumber). IsFinalmarks the last callback for a call.- Order events by
SequenceNumber. It is sent as a string, so compare it as a number. - To catch up on anything you missed, fetch the call with
GET /api/calls/{callSid}.
import express from "express";
const app = express();
app.use(express.json());
// The statusCallback you pass is
// https://your.app/voice/status/<long random token>
const TOKEN = process.env.CALLBACK_TOKEN;
const seen = new Set(); // use your database in production
app.post("/voice/status/:token", (req, res) => {
if (req.params.token !== TOKEN) return res.sendStatus(404);
const sid = req.get("X-CPaaS-Call-Sid");
if (sid !== req.body.CallSid) return res.sendStatus(400);
res.sendStatus(200); // reply first, inside 5 seconds
if (seen.has(req.body.CallbackId)) return; // already handled
seen.add(req.body.CallbackId);
// Check sid against calls you placed, then queue the work.
// Order events by Number(req.body.SequenceNumber).
// req.body.IsFinal === true marks the last callback for the call.
queue.push(req.body);
});curl https://connect.telvox.dev/api/calls/CA7Q2M9X4K1B8N3T6R0W5Y2Z9C1V4H7JL5 \
-H "Authorization: Bearer $TELVOX_API_KEY" \
-H "Accept: application/json"Next: Webhooks & callbacks reference (every request Connect sends) · Calls reference