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-intentscreatedAt 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
Create intent
POST /api/v1/voice/ivr-intentsvoice:write. Role: owner / admin / developer.
Slug (lowercase alphanumeric +
- / _), max 64 chars. Must be unique within the tenant.Natural-language description fed to the classifier as the intent prompt. 1–500 chars.
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).AI-agent id (
agt_*).Whether the classifier should evaluate this intent on live calls.
cURL
Update intent
PATCH /api/v1/voice/ivr-intents/{id}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
Delete intent
DELETE /api/v1/voice/ivr-intents/{id}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
Test classification
POST /api/v1/voice/ivr-intents/testCaller utterance to classify. Max 500 chars (utterances beyond this are truncated by the classifier).
IETF BCP-47 language tag (
en, en-US, tr, es-419, …). Influences classifier prompt locale.cURL
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":
Error model
All errors follow the platform envelope: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.