Concept
Call-control markup
When a call reaches your app, TelVox asks your webhook what to do. Your webhook replies with markup: TwiML-style XML, or a smaller JSON form. TelVox runs the verbs in order against the live call. XML covers every verb on this page. JSON covers a subset.
TelVox tells the two apart by the first character of your reply: < means XML and { means JSON. In XML the <Response> root is optional, so a bare <Dial> works too.
Connect is live. The verbs, attributes and defaults below are what the engine runs today. The API is v0.x, so field names may change before v1.
The webhook loop
Call control is a request and a reply. For an inbound call, TelVox POSTs JSON to the webhook URL set on the number in the dashboard. For an outbound call, it POSTs to the url you passed to POST /api/calls, once the callee answers. Your reply is markup.
Gather, Dial and Record can carry an action URL. When they finish, TelVox sends the result there and runs the markup you return. That is how menus go more than one level deep.
POST /voice/answer HTTP/1.1
Host: your.app
Content-Type: application/json
User-Agent: cPaaS-Platform/1.0
X-CPaaS-Event: webhook.inbound
X-CPaaS-Call-Sid: CA7Q2M9X4K1B8N3T6R0W5Y2Z9C1V4H7JL5
{
"from": "+919876500199",
"to": "+918045670100",
"direction": "inbound"
}<Response>
<Say>Thanks for calling Acme.</Say>
<Gather action="https://your.app/voice/menu" numDigits="1" timeout="5">
<Say>Press 1 for sales, 2 for support.</Say>
</Gather>
</Response>- 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. If an action URL fails, no more markup runs on that call.
- Every request carries
User-Agent: cPaaS-Platform/1.0,X-CPaaS-Event(for examplewebhook.inbound,webhook.outbound,gather.action,dial.actionorrecord.action) andX-CPaaS-Call-Sid. - The inbound body has no call SID. Read it from the
X-CPaaS-Call-Sidheader.
What each action URL receives
Action bodies are JSON. When Dial or Record sets method="GET", the same fields go as query parameters. Gather always POSTs.
| From | Body |
|---|---|
| Gather | {"digit":"1"}. The digits pressed, or an empty string on timeout. |
| Dial | DialCallStatus (completed, no-answer, busy or failed) and DialCallDuration in seconds. RecordingUrl and other Recording… fields when the call was recorded. ConferenceSid (the room name) after a Conference. |
| Record | RecordingName, RecordingUrl and RecordingDuration, plus other Recording… fields. Download the file from RecordingUrl with your API key. |
How verbs run
Verbs run top to bottom. Say, Play, Pause, Wait, Hold and Record finish and move on to the next verb. Gather, Dial, Enqueue, Reject and Hangup end the document: anything after them is ignored. If a document runs out of verbs without ending the call, TelVox hangs up. TelVox skips any tag it does not know, so check your markup against this page.
| Verb | What it does | Then |
|---|---|---|
| <Say> | Speaks text to the caller. | Next verb |
| <Play> | Plays an audio file from a URL. | Next verb |
| <Pause> | Silence for a number of seconds. | Next verb |
| <Wait> | Waits, silent or with hold music. A TelVox extra. | Next verb |
| <Hold> | Plays a hold file for a loop count or a time. A TelVox extra. | Next verb |
| <Record> | Records the caller, for example for voicemail. | Next verb |
| <Gather> | Collects keypresses and posts them to your action URL. | Ends the document |
| <Dial> | Connects the caller to a number, SIP address, conference or queue. | Ends the document |
| <Enqueue> | Puts the caller in a named queue. | Ends the document |
| <Reject> | Ends the call with a busy or rejected reason. | Ends the document |
| <Hangup> | Ends the call. | Ends the document |
XML verb reference
Attribute names are camelCase, as in TwiML. Booleans are the strings true and false. A missing attribute takes the default shown.
<Say>
Speaks the text inside the tag, using Google WaveNet voices. If the text starts with sound: or https://, TelVox plays that audio instead.
| Attribute | Type | Description |
|---|---|---|
| voice | string | woman (default), alice or man. Or a full Google voice name such as en-US-Wavenet-F, which also sets the language. |
| language | string | en-GB (default) or en-US. Any other value uses en-GB. |
<Say voice="man" language="en-US">Your order ships today.</Say>
<Say voice="en-GB-Wavenet-B">Thanks for waiting.</Say><Play>
Plays the audio file whose URL is inside the tag, for example <Play>https://your.app/audio/greeting.wav</Play>. Use a WAV file. There are no attributes and no loop. To speak text, use Say.
<Pause>
Silence, for example between two prompts.
| Attribute | Type | Description |
|---|---|---|
| length | integer | Seconds of silence. Default 1. |
<Gather>
Collects keypad digits (DTMF only, no speech). Put one <Say> inside as the prompt; it uses the default voice. A key pressed during the prompt cuts it short. When enough digits arrive, or the timeout passes, TelVox POSTs {"digit":"…"} to action and runs the markup that comes back. On timeout the digit is an empty string, so your handler decides what happens next.
| Attribute | Type | Description |
|---|---|---|
| action | url | Where TelVox POSTs the digits as JSON. Its reply is the next markup. Without an action, nothing more runs on the call. |
| numDigits | integer | How many digits to collect. Default 1. |
| timeout | integer | Seconds to wait for input after the prompt ends. Default 5. |
<Dial>
Connects the caller to one or more destinations, named by the nouns below. When the dialed call ends, TelVox sends the result to action if you set one. A Dial with a <Queue> or <Conference> noun uses only that noun.
| Attribute | Type | Description |
|---|---|---|
| timeout | integer | Seconds to ring before giving up. Default 30. |
| action | url | Where TelVox sends the result when the dialed call ends. Its reply is the next markup. |
| method | string | POST (default) or GET for the action URL. GET sends the fields as query parameters. |
| callerId | string | The caller ID the dialed party sees. |
| timeLimit | integer | Longest the call may last, in seconds. Default 14400 (4 hours). |
| record | string | record-from-answer (default), record-from-ringing, record-from-answer-dual, record-from-ringing-dual or do-not-record. Dial records unless you set do-not-record. The -dual modes put each side on its own channel where the call allows it; if not, the recording callback's RecordingFallbackReason says why. |
| ringType | string | With several numbers: simultaneous (default, first to answer wins), fixed (one at a time, in order) or round-robin (in order, starting one further along each call). |
| hangupOnStar | boolean | If true, the caller can press * to drop the dialed party. Default false. |
| answerOnBridge | boolean | If true, an inbound caller keeps hearing ringing until the dialed party answers. Only works when Dial is the first verb. Default false. |
| ringTone | string | Country code for the ringback tone, for example in or gb. |
| trim | string | do-not-trim (default) or trim-silence, for the recording. |
| statusCallback | url | Status updates for each dialed leg. A Number or Sip noun can set its own. |
| statusCallbackEvent | string | Space-separated events to send for each leg. |
| statusCallbackMethod | string | POST (default) or GET. |
| recordingStatusCallback | url | Where recording status callbacks go. |
| recordingStatusCallbackEvent | string | Space-separated: in-progress, completed, absent, failed. Default completed. |
| recordingStatusCallbackMethod | string | POST (default) or GET. |
| recordingTrack | string | both (default), inbound or outbound. A bridged recording holds the whole conversation. |
Nouns inside <Dial>:
| Noun | Type | Description |
|---|---|---|
| <Number> | noun | A phone number. Attributes: sendDigits (keys to press after answer, w is a half-second pause), url and method (markup run on the called party before they are connected), statusCallback, statusCallbackEvent, statusCallbackMethod. A bare number inside <Dial> works too. |
| <Sip> | noun | A SIP address such as sip:agent@pbx.example.com. Attributes: username, password, url, method, statusCallback, statusCallbackEvent, statusCallbackMethod. |
| <Conference> | noun | A named room. Everyone whose markup dials the same name joins the same room. Attributes in the table below. |
| <Queue> | noun | A queue name. Connects this call to the caller who has waited longest in that queue (first in, first out). If the queue is empty, DialCallStatus is no-answer. |
<Conference> attributes. Room settings come from the attributes of whoever joins.
| Attribute | Type | Description |
|---|---|---|
| muted | boolean | Join muted: can hear, can't speak. Default false. |
| beep | string | true (default), false, onEnter or onExit. |
| startConferenceOnEnter | boolean | Default true. If false, this caller hears hold music until someone with true joins. |
| endConferenceOnExit | boolean | If true, the room ends for everyone when this caller leaves. Default false. |
| maxParticipants | integer | Most people allowed in the room. Default 250. |
| waitUrl | url | Markup played while waiting for the room to start (Say, Play and Wait only). Otherwise hold music. |
| waitMethod | string | POST (default) or GET. |
| record | string | do-not-record (default) or record-from-start. |
| trim | string | do-not-trim or trim-silence. |
| statusCallback | url | Where conference events go. |
| statusCallbackEvent | string | Space-separated: start, end, join, leave. Default all four. |
| statusCallbackMethod | string | POST (default) or GET. |
| recordingStatusCallback | url | Recording status callbacks for the room recording. Also recordingStatusCallbackEvent and recordingStatusCallbackMethod. |
| participantLabel | string | A label for this caller, sent back in conference events. |
<Response>
<Dial callerId="+918045670100" timeout="20" ringType="fixed"
action="https://your.app/voice/after-dial">
<Number>+919876500199</Number>
<Number>+919876500188</Number>
</Dial>
</Response><Record>
Records the caller, for example for voicemail, then moves on to the next verb. TelVox does not transcribe recordings. To record a connected call, use the record attribute on Dial instead. If you set action, its reply runs first, and any verbs after <Record> still run afterwards unless that reply ended the call.
| Attribute | Type | Description |
|---|---|---|
| maxLength | integer | Longest recording, in seconds. Default 60. |
| finishOnKey | string | Key that stops the recording. Default #. |
| timeout | integer | Seconds of silence that stop the recording. Default 5. |
| playBeep | boolean | Play a beep before recording starts. Default false. |
| action | url | Where TelVox sends RecordingName, RecordingUrl and RecordingDuration when the recording ends. Its reply runs next. |
| method | string | POST (default) or GET for the action URL. |
| format | string | wav (default). |
| recordingStatusCallback | url | Where recording status callbacks go. |
| recordingStatusCallbackEvent | string | Space-separated: in-progress, completed, absent. Default completed. |
| recordingStatusCallbackMethod | string | POST (default) or GET. |
<Response>
<Say>Please leave a message after the beep.</Say>
<Record maxLength="120" playBeep="true"
recordingStatusCallback="https://your.app/voice/rec-status" />
<Say>Thanks. Goodbye.</Say>
<Hangup/>
</Response><Hangup>
Ends the call: <Hangup/>. No attributes.
<Reject>
Ends the call with a busy or rejected reason (SIP 486 or 603) instead of a normal hangup.
| Attribute | Type | Description |
|---|---|---|
| reason | string | rejected (default, SIP 603) or busy (SIP 486). |
<Enqueue>
Puts the caller in the queue named inside the tag, for example <Enqueue>sales</Enqueue>. The caller waits until another call runs <Dial><Queue>sales</Queue></Dial>, or until maxWait runs out. Queues are first in, first out. Verbs after <Enqueue> do not run. The Queue tab in the Dial samples above shows both sides.
| Attribute | Type | Description |
|---|---|---|
| waitUrl | url | Markup played on a loop while the caller waits (Say, Play and Wait only). Otherwise hold music. |
| waitMethod | string | POST (default) or GET. |
| maxWait | integer | Longest wait in seconds before the caller leaves the queue. Default 300 (5 minutes). |
<Wait>
A TelVox extra. Waits for a set time, silent or with hold music, then moves on: <Wait music="true" length="15" />.
| Attribute | Type | Description |
|---|---|---|
| length | integer | Seconds to wait. Default 30. |
| music | boolean | true plays hold music. Default false (silent). |
| mohClass | string | Which hold-music class to play. Default default. |
<Hold>
A TelVox extra. Plays a hold file you choose, then moves on: <Hold music="https://your.app/audio/hold.wav" loop="3" />.
| Attribute | Type | Description |
|---|---|---|
| musicrequired | string | A sound name (sound:name) or an audio URL. Without it the verb is skipped. |
| loop | integer | How many times to play it. Default 1. |
| length | integer | Cap in seconds. With loop set too, whichever comes first ends the hold. |
JSON markup (a subset)
If you would rather return JSON, reply with an object whose response key holds the verbs in order. The root must be that object: a bare array is rejected.
say: a string, or{ text, voice, language }.play: an audio URL string.dial: a number string, or an object withnumber,numbers,sip,conferenceorqueueName, plus most of the Dial attributes above in the same camelCase.wait:{ length, music, mohClass }.hold: a music string, or{ music, loop, length }.enqueue: a queue name, or{ queueName, waitUrl, maxWait }.hangup:true.
JSON has no gather, record, reject or pause. Use XML for menus and voicemail.
{
"response": [
{ "say": "Connecting you to the duty engineer." },
{ "play": "https://your.app/audio/chime.wav" },
{
"dial": {
"numbers": ["+919876500199", "+919876500188"],
"ringType": "simultaneous",
"timeout": 20,
"action": "https://your.app/voice/after-dial"
}
}
]
}Not supported today
If you are moving from TwiML, these verbs, nouns and options do not run on Connect. TelVox skips the tags, so the rest of the document carries on without them.
- Leave
- Refer
- Connect
- Stream and media streams
- Start and Stop
- Pay
- Prompt
- Echo
- Client, Sim and Application nouns
- Speech input on Gather
- finishOnKey and method on Gather
- loop on Play
- Transcription
A small menu, end to end
The webhook greets the caller and offers a one-digit menu. Gather ends the document, so the goodbye lives in the action handler: it reads req.body.digit, sends 1 to sales, puts 2 in the support queue, and says goodbye when the digit is empty or unknown. If sales does not answer, the Dial action takes a voicemail.
<Response>
<Say>Thanks for calling Acme.</Say>
<Gather action="https://your.app/voice/menu" numDigits="1" timeout="5">
<Say>Press 1 for sales, 2 for support.</Say>
</Gather>
</Response>import express from "express";
const app = express();
app.use(express.json());
const xml = (res, body) =>
res.type("text/xml").send(`<Response>${body}</Response>`);
// The number's webhook URL (or the url you passed to POST /api/calls)
app.post("/voice/answer", (req, res) => {
xml(res, `
<Say>Thanks for calling Acme.</Say>
<Gather action="https://your.app/voice/menu" numDigits="1" timeout="5">
<Say>Press 1 for sales, 2 for support.</Say>
</Gather>`);
});
// Gather's action URL. The body is {"digit":"1"}, or "" on timeout.
app.post("/voice/menu", (req, res) => {
const digit = req.body.digit;
if (digit === "1") {
return xml(res, `
<Dial timeout="20" action="https://your.app/voice/after-dial">
<Number>+919876500199</Number>
</Dial>`);
}
if (digit === "2") {
return xml(res, `<Enqueue maxWait="300">support</Enqueue>`);
}
xml(res, `<Say>Sorry, we didn't get that. Goodbye.</Say><Hangup/>`);
});
// Dial's action URL. DialCallStatus is completed, no-answer, busy or failed.
app.post("/voice/after-dial", (req, res) => {
if (req.body.DialCallStatus === "completed") {
return xml(res, `<Hangup/>`);
}
xml(res, `
<Say>Sales is busy. Leave a message after the beep.</Say>
<Record maxLength="120" playBeep="true" />
<Hangup/>`);
});
app.listen(3000);Next: Webhooks & callbacks · Calls reference