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.

API stability

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.

POSTyour webhook
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"
}
your app → TelVox
<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>
200 ok · text/xml
  • 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 example webhook.inbound, webhook.outbound, gather.action, dial.action or record.action) and X-CPaaS-Call-Sid.
  • The inbound body has no call SID. Read it from the X-CPaaS-Call-Sid header.

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.

FromBody
Gather{"digit":"1"}. The digits pressed, or an empty string on timeout.
DialDialCallStatus (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.
RecordRecordingName, 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.

VerbWhat it doesThen
<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.

AttributeTypeDescription
voicestringwoman (default), alice or man. Or a full Google voice name such as en-US-Wavenet-F, which also sets the language.
languagestringen-GB (default) or en-US. Any other value uses en-GB.
say
<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.

AttributeTypeDescription
lengthintegerSeconds 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.

AttributeTypeDescription
actionurlWhere TelVox POSTs the digits as JSON. Its reply is the next markup. Without an action, nothing more runs on the call.
numDigitsintegerHow many digits to collect. Default 1.
timeoutintegerSeconds 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.

AttributeTypeDescription
timeoutintegerSeconds to ring before giving up. Default 30.
actionurlWhere TelVox sends the result when the dialed call ends. Its reply is the next markup.
methodstringPOST (default) or GET for the action URL. GET sends the fields as query parameters.
callerIdstringThe caller ID the dialed party sees.
timeLimitintegerLongest the call may last, in seconds. Default 14400 (4 hours).
recordstringrecord-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.
ringTypestringWith 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).
hangupOnStarbooleanIf true, the caller can press * to drop the dialed party. Default false.
answerOnBridgebooleanIf true, an inbound caller keeps hearing ringing until the dialed party answers. Only works when Dial is the first verb. Default false.
ringTonestringCountry code for the ringback tone, for example in or gb.
trimstringdo-not-trim (default) or trim-silence, for the recording.
statusCallbackurlStatus updates for each dialed leg. A Number or Sip noun can set its own.
statusCallbackEventstringSpace-separated events to send for each leg.
statusCallbackMethodstringPOST (default) or GET.
recordingStatusCallbackurlWhere recording status callbacks go.
recordingStatusCallbackEventstringSpace-separated: in-progress, completed, absent, failed. Default completed.
recordingStatusCallbackMethodstringPOST (default) or GET.
recordingTrackstringboth (default), inbound or outbound. A bridged recording holds the whole conversation.

Nouns inside <Dial>:

NounTypeDescription
<Number>nounA 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>nounA SIP address such as sip:agent@pbx.example.com. Attributes: username, password, url, method, statusCallback, statusCallbackEvent, statusCallbackMethod.
<Conference>nounA named room. Everyone whose markup dials the same name joins the same room. Attributes in the table below.
<Queue>nounA 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.

AttributeTypeDescription
mutedbooleanJoin muted: can hear, can't speak. Default false.
beepstringtrue (default), false, onEnter or onExit.
startConferenceOnEnterbooleanDefault true. If false, this caller hears hold music until someone with true joins.
endConferenceOnExitbooleanIf true, the room ends for everyone when this caller leaves. Default false.
maxParticipantsintegerMost people allowed in the room. Default 250.
waitUrlurlMarkup played while waiting for the room to start (Say, Play and Wait only). Otherwise hold music.
waitMethodstringPOST (default) or GET.
recordstringdo-not-record (default) or record-from-start.
trimstringdo-not-trim or trim-silence.
statusCallbackurlWhere conference events go.
statusCallbackEventstringSpace-separated: start, end, join, leave. Default all four.
statusCallbackMethodstringPOST (default) or GET.
recordingStatusCallbackurlRecording status callbacks for the room recording. Also recordingStatusCallbackEvent and recordingStatusCallbackMethod.
participantLabelstringA label for this caller, sent back in conference events.
dial
<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.

AttributeTypeDescription
maxLengthintegerLongest recording, in seconds. Default 60.
finishOnKeystringKey that stops the recording. Default #.
timeoutintegerSeconds of silence that stop the recording. Default 5.
playBeepbooleanPlay a beep before recording starts. Default false.
actionurlWhere TelVox sends RecordingName, RecordingUrl and RecordingDuration when the recording ends. Its reply runs next.
methodstringPOST (default) or GET for the action URL.
formatstringwav (default).
recordingStatusCallbackurlWhere recording status callbacks go.
recordingStatusCallbackEventstringSpace-separated: in-progress, completed, absent. Default completed.
recordingStatusCallbackMethodstringPOST (default) or GET.
record
<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.

AttributeTypeDescription
reasonstringrejected (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.

AttributeTypeDescription
waitUrlurlMarkup played on a loop while the caller waits (Say, Play and Wait only). Otherwise hold music.
waitMethodstringPOST (default) or GET.
maxWaitintegerLongest 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" />.

AttributeTypeDescription
lengthintegerSeconds to wait. Default 30.
musicbooleantrue plays hold music. Default false (silent).
mohClassstringWhich 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" />.

AttributeTypeDescription
musicrequiredstringA sound name (sound:name) or an audio URL. Without it the verb is skipped.
loopintegerHow many times to play it. Default 1.
lengthintegerCap 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 with number, numbers, sip, conference or queueName, 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.

answer.json
{
  "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"
      }
    }
  ]
}
200 ok · application/json

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.

answer.xml
<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>
200 ok · text/xml
your webhook server
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