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).

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.
name
string
required
Slug (lowercase alphanumeric + - / _), max 64 chars. Must be unique within the tenant.
description
string
required
Natural-language description fed to the classifier as the intent prompt. 1–500 chars.
target_queue_id
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).
target_agent_id
string
AI-agent id (agt_*).
active
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.
utterance
string
required
Caller utterance to classify. Max 500 chars (utterances beyond this are truncated by the classifier).
language
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.

See also