Designing Voice IVR Menus
A well-designed IVR gets callers to the right place in seconds. This guide covers menu structure, prompt writing, choosing DTMF versus speech, and handling the inevitable mistakes.
🚀 Quick Start
curl -X POST https://api.afriroute.ai/api/v1/voice/calls \
-H "Authorization: Bearer $AFRIROUTE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "+254712345678",
"from": "+254700000000",
"flow": {
"say": "Welcome to Acme. Press 1 for sales, 2 for support.",
"gather": { "input": "dtmf", "num_digits": 1, "action_url": "https://example.com/ivr" }
}
}'
🧭 Menu Structure
Keep menus shallow and option counts low. Callers can't hold more than a few choices in memory.
| Rule | Target |
|---|---|
| Options per menu | 4 or fewer |
| Menu depth | 3 levels max |
| Time to first menu | Under 5 seconds |
| Repeat option | Always include |
✍️ Writing Prompts
- State the action before the key: "For support, press 2" — not "Press 2 for support."
- Put the most-used option first.
- Keep each prompt under 7 seconds.
- Always offer "press 0 for an operator" and "press star to repeat".
{
"say": "For account balance, press 1. To make a payment, press 2. To speak to an agent, press 0.",
"gather": { "input": "dtmf", "num_digits": 1, "timeout": 5 }
}
🔢 DTMF vs Speech
| Input | Best for | Watch out for |
|---|---|---|
| DTMF (keypad) | Menus, PINs, amounts | Limited to numbers |
| Speech | Open questions, names, addresses | Accents, noise, cost |
For African deployments with many languages and noisy environments, DTMF is more reliable for menus. Reserve speech for open-ended capture and pair it with confidence checks — see Speech Recognition.
const flow = {
say: 'Say the city you are calling about, or press 0 for an agent.',
gather: { input: 'speech dtmf', language: 'en-KE', action_url: 'https://example.com/ivr/city' }
};
⚠ ️ Error Handling
Handle the three failure modes explicitly: no input, invalid input, and repeated failure.
def on_gather(req):
if req['digits'] == '': # timeout / no input
return reprompt(attempt=req['attempt'])
if req['digits'] not in VALID: # invalid key
return invalid_prompt(req['attempt'])
return route(req['digits'])
def reprompt(attempt):
if attempt >= 2:
return {"say": "Transferring you to an agent.", "dial": "+254700000000"}
return {"say": "I didn't catch that. Please try again.", "gather": MENU}
💡 Best Practices
- Cap retries at 2, then fall back to a human.
- Confirm critical inputs (amounts, account numbers) by reading them back.
- Let callers interrupt prompts (barge-in) once they know the menu.
- Localize prompts by caller country or a language-select first step.
- Log path drop-off to find confusing menus.
📚 Related Resources
Last Updated: May 2026