TryVox

Building an IVR

Build a phone menu with Gather — prompts, branching, retries and routing to a human.

An IVR is a loop: play a prompt, collect key presses, decide what to do next. In VoxML that is a Gather verb whose action_url returns the next document.

caller dials  →  your answer_url returns Gather
              →  caller presses a key
              →  TryVox POSTs the digits to action_url
              →  you return the next VoxML  →  repeat or finish

Your server holds all the logic; TryVox plays prompts and reports what was pressed.

Only DTMF (key presses) is collected. Speech input is not implemented — speech_result is always empty. See Gather.

A working main menu

Your number's answer webhook returns:

{
  "voxml_version": "1.0",
  "instructions": [
    {
      "verb": "Gather",
      "action_url": "https://example.com/ivr/main",
      "num_digits": 1,
      "timeout": 6,
      "say": {
        "text": "Thanks for calling Acme. For sales, press 1. For support, press 2. To hear this again, press 9."
      }
    }
  ]
}

TryVox plays the prompt, waits, then POSTs to action_url:

{
  "call_uuid": "abc-123",
  "account_id": "TJ4530defa",
  "digits": "1",
  "speech_result": "",
  "reason": "complete",
  "from": "+919876543210",
  "to": "+911234567890"
}

Your handler returns the next document:

@app.post("/ivr/main")
def main_menu(req):
    digits = req.json["digits"]
    reason = req.json["reason"]

    if reason != "complete" or digits == "":
        return reprompt("Sorry, I didn't get that.")

    if digits == "1":
        return {"voxml_version": "1.0", "instructions": [
            {"verb": "Say",  "text": "Connecting you to sales."},
            {"verb": "Dial", "number": "+919000000001"},
        ]}

    if digits == "2":
        return {"voxml_version": "1.0", "instructions": [
            {"verb": "Redirect", "url": "https://example.com/ivr/support"}
        ]}

    if digits == "9":
        return {"voxml_version": "1.0", "instructions": [
            {"verb": "Redirect", "url": "https://example.com/ivr/start"}
        ]}

    return reprompt("That isn't one of the options.")

Returning an empty instruction list ends the call, so always return something.

Handling no input and wrong input

digits alone cannot tell you whether the caller chose nothing or was cut off. That is what reason is for:

reasonWhat happenedUsual response
completeEntered num_digits or pressed finish_on_keyAct on the digits
timeoutThe caller went quietReprompt, then fall back
cancelledThe call endedNothing — the call is gone
errorInput could not be collectedTreat as no input

A timeout is normal, not a failure. Give callers two attempts and then route them somewhere a human or a voicemail can take over:

def reprompt(message, attempt=1):
    if attempt >= 3:
        return {"voxml_version": "1.0", "instructions": [
            {"verb": "Say",  "text": "Let me put you through to someone."},
            {"verb": "Dial", "number": "+919000000000"},
        ]}
    return {"voxml_version": "1.0", "instructions": [
        {"verb": "Gather",
         "action_url": f"https://example.com/ivr/main?attempt={attempt + 1}",
         "num_digits": 1,
         "say": {"text": f"{message} For sales, press 1. For support, press 2."}}
    ]}

Track the attempt count in your own action_url — the query string is the simplest place, since TryVox posts back to exactly the URL you supply.

Collecting more than one digit

For an account number or PIN, set num_digits to the expected length. The Gather returns as soon as that many keys are pressed, so the caller is not held for the rest of the timeout:

{
  "verb": "Gather",
  "action_url": "https://example.com/ivr/account",
  "num_digits": 8,
  "timeout": 15,
  "say": { "text": "Please enter your 8 digit account number." }
}

When the length varies, drop num_digits and let the caller signal that they are finished with finish_on_key (default #):

{
  "verb": "Gather",
  "action_url": "https://example.com/ivr/reference",
  "finish_on_key": "#",
  "timeout": 20,
  "say": { "text": "Enter your reference number, then press hash." }
}

The terminator is stripped from digits — it means "I'm done", it is not part of the entry. A caller who enters 4321# gives you 4321.

Callers who know the menu often type before the prompt finishes. Those presses are buffered and count toward the Gather, so dial-ahead works without them having to type twice.

Nested menus

There is nothing special about a submenu — it is another Gather returned from the previous one:

{
  "voxml_version": "1.0",
  "instructions": [
    {
      "verb": "Gather",
      "action_url": "https://example.com/ivr/support",
      "num_digits": 1,
      "say": { "text": "For billing, press 1. For technical support, press 2. To go back, press 0." }
    }
  ]
}

Keep the depth shallow. Two levels is usually enough, and a "press 0 to go back" option costs one line and saves callers from being stuck.

Ending the call

Route to a person with Dial, take a message with Record, or finish with Hangup:

{
  "voxml_version": "1.0",
  "instructions": [
    { "verb": "Say", "text": "Please leave a message after the tone." },
    { "verb": "Record",
      "action_url": "https://example.com/ivr/voicemail",
      "max_length": 120,
      "finish_on_key": "#" },
    { "verb": "Say", "text": "Thank you. Goodbye." },
    { "verb": "Hangup" }
  ]
}

Practical notes

  • Prompt inside the Gather, not before it. A Say placed before a Gather finishes playing before collection starts, so anything pressed during it is buffered rather than acted on immediately.
  • Keep prompts short. Callers press as soon as they hear their option.
  • Set timeout to suit the input. Six seconds for a menu choice is plenty; an account number needs longer.
  • action_url receives a POST with a JSON body by default. Set method if you need GET.
  • Validate call_uuid against your own records before acting on anything sensitive.
  • Gather — full parameter reference
  • Dial — connect the caller to a person
  • Record — take a voicemail
  • Redirect — hand control to another document

On this page