Skip to main content
An NLU-routed IVR replaces the “press 1 for sales” pattern with free-form speech: the caller says what they want, a classifier scores their utterance against your intent buckets, and the call routes to the matching ACD queue or AI agent. The endpoint reference is the IVR intents page — this guide takes one routing problem end to end: a support-triage menu (sales, billing, support) built, tested, fallback-hardened, and wired into an IVR flow.

1. When NLU beats DTMF

DTMF menus are still the right tool for short, closed-option menus (“press 1 to hear your balance”). Reach for NLU when any of these apply:
  • Wide or flat option sets. Once the menu runs past 3–4 options, DTMF abandonment climbs. One spoken utterance beats “press 1, then 3, then 2”.
  • Open-ended requests. DTMF trees can only encode options you anticipated. NLU routes utterances you never planned a branch for — “someone charged me twice” maps to billing without a dedicated key.
  • Accessibility and mobile callers. Spoken requests beat keypad entry for callers on hands-free devices.
  • Continuous improvement. Fallback analytics show the utterances that failed to match, so you add intents for real caller language instead of designing menus in the dark.
The trade-off: NLU adds one classification round-trip (P95 under 1s) and behaves probabilistically — you manage the threshold yourself (section 4). A common middle ground is DTMF for the first split, NLU for the sub-route.

2. Model your intents

An intent is a named bucket (object fields). Three rules of thumb:
  • Name it like a handle. Lowercase alphanumeric plus - / _, max 64 chars, unique per tenant: billing, sales-eu, tech_support. You branch on this slug in your flow, so keep it stable — the API id (ivri_*) survives renames, the slug is what your code reads.
  • Write a rich description. The classifier evaluates the utterance against the description text, not the slug. “Billing” matches little; “Caller has a question about an invoice, charge, payment, or refund” matches a lot. Name the real vocabulary callers use.
  • Analytics-only intents are valid. Leave both route fields null and the match is recorded without routing — useful for sizing a new queue before you staff it, or for topic reporting. See section 3.
Keep the active set under 50 intents — the classifier caps each prompt at 50 and slices anything past that, so burying your best intents at positions 51+ silently drops them. The full constraint list lives on the reference page.

3. Route to queues or AI agents

Each intent routes to exactly one destination: The two fields are mutually exclusive — set both and create/update returns 422 VALIDATION_ERROR. Set neither and you get an analytics-only intent. To switch an intent’s destination, PATCH one field to the new value and the other to null.

4. Test utterances before going live

POST /api/v1/voice/ivr-intents/test classifies an utterance against your active intents without placing a call. Use it as a pre-publish gate: candidate descriptions that can’t beat the threshold stay out of production. The contract: a match commits at confidence ≥ 0.7; below that, matched_intent_id is null and fallback_reason is one of: The response also carries the model’s reasoning, so a failed match tells you which direction to move the description. Note: a 422 VALIDATION_ERROR here only means the request shape was wrong (missing utterance or over 500 chars) — a clean no_match runs the classifier anyway so you see why an empty active set can’t route.

5. Worked example: support triage

Build a three-way triage — sales, billing, support.
cURL
Repeat with billingque_billing and supportque_support. If your support destination is an AI agent, send target_agent_id: "agt_..." instead — the classifier doesn’t care which target type sits behind the slug. Now test candidate utterances:
cURL
200 OK — above threshold:
200 OK — below threshold:
Below threshold means the flow should take the fallback edge, not pick the weak match. Keep a fixture list of utterances per intent and re-run the tests whenever you change a description — treat it as a routing regression harness, the same role the flow simulator plays for graph edges.

6. Apply the intents in an IVR flow

A flow that speaks, gathers one utterance, classifies it, and branches:
The classifier runs at the speechInput node; edges from it branch by intent slug, and unmatched utterances take the default edge. The voice guides — the flow builder walkthrough for graph validation, simulation, and publishing, and the voice quickstart for the inbound attach path — run in parallel with the visual Flow Builder, which shares the same node palette. Use either editing surface; the published graph is what the runtime walks.
Outbound termination stays on the Devotel softswitch. A transfer node pointed at a raw SIP URI fails validation with transfer_external_sip — route by E.164 number or on-net extension.
Simulate the fallback path too — send an empty turn list and confirm the caller lands on voicemail rather than hanging in the classifier node.

7. Multilingual calls

Set language per classification to a BCP-47 tag (en-US, tr, es-419, …). The tag goes into the classifier prompt, so it changes how the model weighs the utterance — pass the real caller locale rather than defaulting to en. If you serve multilingual queues, re-run your section-5 fixture utterances per language; a description that reads well in English can still be thin for another locale. Keep the descriptions themselves in one language and let the tag do the work — mixing languages inside the same description hurts all of them.

8. Observability and hygiene

  • Audit logs. Every create / update / delete / test writes an audit entry (voice.ivr_intent.<verb>), so you can see who changed which description and when — trace a routing regression back to the edit.
  • Prompt hygiene. The classifier builds its prompt from the active set. Deactivate rather than delete during experiments: deactivation drops the intent out of classification without erasing its edit history, and reactivation is one PATCH of active: true.
  • Deactivate noisy intents. An intent that keeps matching the wrong utterances skews live routing — drop it out of the active set while you rewrite the description.
  • Watch your base rate. Frequent ambiguous fallback on intent pairs (sales vs billing, support vs returns) usually means both descriptions phrase the same middle ground — differentiate by the decisive word (invoice vs pricing, broken vs refund).

9. Troubleshooting

Low-confidence clusters. If tests against one intent keep landing in 0.4–0.69, the description is thin. Add the callers’ literal vocabulary (“invoice”, “charged”, “refund”, “payment plan”) and retest — a 500-char budget is room for a lot of synonyms. Ambiguous ties. Split by the decisive feature — put “upgrade / pricing / new order” into sales and “invoice / charge / refund” into billing — then rerun the utterances that tied. Empty active set. Every intent deleted or active: false returns fallback_reason: "no_match" on every classification — if the dashboard route seems frozen but live calls keep hitting the fallback edge, check for an all-inactive set. That state is valid for analytics but routes nothing. Duplicate-name conflicts. 409 IVR_INTENT_NAME_CONFLICT on create, or a 409 raced against another writer. GET / first and reuse one slug set across your routing code and flow edges.

See also