Skip to main content

IVR Intents

Conversational IVR intents replace traditional DTMF menus (“Press 1 for sales”) with natural-language utterance classification. The caller says what they want; the platform runs the utterance through a per-tenant set of intent buckets and routes the call to the matching ACD queue or AI agent. Each intent is a named bucket (sales, billing, support, …) with a natural-language description that the NLU classifier (Claude Haiku) compares against the live caller utterance. Base path: /api/v1/voice/ivr-intents Authentication: Clerk session (Authorization: Bearer <token>) or API key (X-API-Key). Scopes:

Intent object

Both target_queue_id and target_agent_id may be null — that’s a valid pure-analytics intent (the call is classified for reporting but not routed).

DTMF-free speech menus

There are two ways to route on speech, and they compose:
  • Intent buckets (this page) — tenant-level descriptions the LLM classifier evaluates against the utterance. Best for open-ended routing across a whole tenant (“sales” vs “billing” vs “support”).
  • Speech Input node grammar — per-node speechIntents phrases edited on the node itself. Matching is deterministic phrase-versus-transcript string math (normalised exact match, word-order tolerance, partial phrase coverage) — no classifier round trip runs in the live gather. Best for a handful of expected utterances at one menu point.
The two layers are independent: a bucket description never feeds a node’s phrase match, and a node’s phrases are never sent to the classifier. For the node-level path, the IVR Builder exposes a keypad fallback toggle and a min-confidence gate on each Speech Input node. Turn the fallback off when you genuinely want speech-only — for example, a menu whose digits were never announced to callers, or an accessibility-led flow where the keypad creates confusion. Before the toggle existed, every speech node silently behaved as if keypad fallback were on; a legacy flow stays digit-enabled until an operator turns it off, and each intent can still carry one optional keypad digit while fallback is on (the digit always wins over speech, so a caller who can’t be understood still routes). The min-confidence gate (0–1) works the same way on both paths: a transcript recognised below the gate is a no-match. On the node path, the walk takes the node’s nomatch edge; set the confidenceThreshold node field alongside the tenant-level classifier threshold so a noisy line falls to a reprompt or an agent rather than a wrong branch. Editing the phrases, the toggle, the provider, and the gate is documented in the grammar-editor section of Build and ship your first IVR flow.

List intents

GET /api/v1/voice/ivr-intents
Returns every intent for the calling tenant, ordered by createdAt ASC. Includes inactive intents so the dashboard can render an enable/disable toggle. Defense-in-depth fallback (DEVOTEL-ORBIT-5H): If the tenant’s 287_ivr_intents migration has not yet applied (new tenant whose just-in-time migration runner is still executing, or a restored-from-backup tenant), this endpoint returns an empty array data: [] rather than a 503. Four Postgres provisioning-gap SQLSTATEs are swallowed: 42P01 (undefined_table), 42703 (undefined_column), 3F000 (invalid_schema_name) and 42704 (undefined_object); any other failure propagates as a normal error. Transient connection blips (pgbouncer drain / Cloud SQL pool pressure — CONNECTION_CLOSED, ECONNRESET, 53300) are retried first (DEVOTEL-ORBIT-6D) and only degrade to the empty-list fallback if they are genuinely a provisioning gap.
cURL
200 OK

Create intent

POST /api/v1/voice/ivr-intents
Scope: voice:write. Role: owner / admin / developer.
string
required
Slug (lowercase alphanumeric + - / _), max 64 chars. Must be unique within the tenant.
string
required
Natural-language description fed to the classifier as the intent prompt. 1–500 chars.
string
ACD queue id (1–128 chars, e.g. que_billing_eu). Mutually exclusive with target_agent_id (enforced at the app layer; both may be omitted for analytics-only intents).
string
AI-agent id (agt_*).
boolean
default:"true"
Whether the classifier should evaluate this intent on live calls.
cURL
201 Created — returns the created row. Errors

Update intent

PATCH /api/v1/voice/ivr-intents/{id}
Scope: voice:write. Role: owner / admin / developer. Partial update. Only fields present in the body overwrite the existing row. Send target_queue_id: null to clear a route.
cURL
200 OK — returns the updated row. 404 NOT_FOUND — id not found in the tenant schema.

Delete intent

DELETE /api/v1/voice/ivr-intents/{id}
Scope: voice:write. Role: owner / admin / developer. Hard delete. The intent will no longer appear in any future classifier prompt or list response. Existing call routings already in flight are not affected (routing decisions are recorded on call_logs at decision time, not by FK reference).
cURL
204 No Content on success. 404 NOT_FOUND — id not found.

Test classification

POST /api/v1/voice/ivr-intents/test
Ad-hoc classifier preview. Runs the supplied utterance against the tenant’s active intents and returns the matched intent (if any) plus the classifier’s confidence. Use this from the dashboard’s IVR studio to validate a new intent description without placing a real call.
string
required
Caller utterance to classify. Max 500 chars (utterances beyond this are truncated by the classifier).
string
default:"en"
IETF BCP-47 language tag (en, en-US, tr, es-419, …). Influences classifier prompt locale.
cURL
200 OK
On a match, fallback_reason is absent. When no active intent matches above the confidence threshold (0.7), matched_intent_id is null and fallback_reason carries one of "low_confidence" (a candidate scored below threshold), "no_match" (no plausible mapping), or "ambiguous" (two or more intents tied). There is no boolean fallback field. An empty active intent set (no rows or all active: false) is a valid state — the classifier still runs and returns fallback_reason: "no_match":
Performance: P95 < 1s (Haiku call ~250ms + intent fetch + audit log).

Error model

All errors follow the platform envelope:
Branch on error.code, not error.message (messages may be localised).

Observability

Every state change (create, update, delete, test) writes a structured audit log with action=voice.ivr_intent.<verb> and the resource id, available via the Audit Logs API.

Speech-gather grammar (DTMF-free routing, NLU-tuned)

The tenant intent buckets above are one way to route by speech — a classifier decision per utterance. For menus that need a deterministic, no-LLM match and true DTMF-free operation, the flow’s Speech node (speechInput) also accepts an inline grammar of caller phrases: matching is exact phrase matching against the transcript, not a model call, so the same phrase routes the same way on every call. Declare the grammar on the Speech node’s data under speechIntents; each intent’s name is the branch handle you wire out of the node:
Edge precedence at the Speech node: a pressed digit routing to a wired dtmfKey wins; otherwise the transcript is matched against the phrases; no match (or confidence below the threshold) takes the nomatch branch, then default; a silent caller retries within the gather’s retry budget and then takes the timeout branch, then default. Pick the inline grammar for short, closed menus (“press or say”) and the tenant intent buckets for open-ended requests — the two can share one flow (guide). Turn on detectLanguage on the Speech node to pin a multilingual caller’s language for the rest of the call.

See also