> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbit.devotel.io/llms.txt
> Use this file to discover all available pages before exploring further.

# IVR Intents

> Conversational NLU intent buckets that route inbound voice callers to ACD queues or AI agents

# 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:**

| Endpoint                              | Scope         | Roles allowed                   |
| ------------------------------------- | ------------- | ------------------------------- |
| `GET /`                               | `voice:read`  | any                             |
| `POST /test`                          | `voice:read`  | any                             |
| `POST /`, `PATCH /:id`, `DELETE /:id` | `voice:write` | `owner` / `admin` / `developer` |

***

## Intent object

| Field             | Type           | Notes                                                                                              |
| ----------------- | -------------- | -------------------------------------------------------------------------------------------------- |
| `id`              | string         | `ivri_<32 hex>`. Stable across renames.                                                            |
| `name`            | string         | Lowercase alphanumeric + `-` / `_`, max 64 chars. Unique per tenant.                               |
| `description`     | string         | Natural-language prompt the classifier evaluates against the utterance (max 500 chars).            |
| `target_queue_id` | string \| null | ACD queue id (1–128 chars, e.g. `que_sales_us`) to route matched calls to.                         |
| `target_agent_id` | string \| null | AI-agent id to route matched calls to. Mutually exclusive with `target_queue_id` at the app layer. |
| `active`          | boolean        | Inactive intents are excluded from classifier prompts. Default `true`.                             |
| `createdAt`       | string         | ISO-8601.                                                                                          |
| `updatedAt`       | string         | ISO-8601.                                                                                          |

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

<Note>
  `GET /api/v1/voice/ivr-intents`
</Note>

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.

```bash cURL theme={null}
curl "https://api.orbit.devotel.io/api/v1/voice/ivr-intents" \
  -H "X-API-Key: dv_live_sk_your_key_here"
```

**200 OK**

```json theme={null}
{
  "data": [
    {
      "id": "ivri_3f1c8b2a9d4e5f7a8b6c1d2e3f4a5b6c",
      "name": "sales",
      "description": "Caller wants to buy a product or ask about pricing.",
      "target_queue_id": "que_sales_us",
      "target_agent_id": null,
      "active": true,
      "createdAt": "2026-05-22T10:30:00Z",
      "updatedAt": "2026-05-22T10:30:00Z"
    }
  ],
  "meta": { "request_id": "req_abc123", "timestamp": "2026-05-24T12:00:00Z" }
}
```

***

## Create intent

<Note>
  `POST /api/v1/voice/ivr-intents`
</Note>

**Scope:** `voice:write`. **Role:** `owner` / `admin` / `developer`.

<ParamField body="name" type="string" required>
  Slug (lowercase alphanumeric + `-` / `_`), max 64 chars. Must be unique within the tenant.
</ParamField>

<ParamField body="description" type="string" required>
  Natural-language description fed to the classifier as the intent prompt. 1–500 chars.
</ParamField>

<ParamField body="target_queue_id" type="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).
</ParamField>

<ParamField body="target_agent_id" type="string">
  AI-agent id (`agt_*`).
</ParamField>

<ParamField body="active" type="boolean" default="true">
  Whether the classifier should evaluate this intent on live calls.
</ParamField>

```bash cURL theme={null}
curl -X POST "https://api.orbit.devotel.io/api/v1/voice/ivr-intents" \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "billing",
    "description": "Caller has a question about an invoice, charge, or payment.",
    "target_queue_id": "que_billing_eu",
    "active": true
  }'
```

**201 Created** — returns the created row.

**Errors**

| Status | `error.code`               | Cause                                                                                     |
| ------ | -------------------------- | ----------------------------------------------------------------------------------------- |
| `409`  | `IVR_INTENT_NAME_CONFLICT` | An intent with this `name` already exists for the tenant.                                 |
| `422`  | `VALIDATION_ERROR`         | `name` fails the regex, `description` is empty/too long, or both `target_*` keys are set. |
| `500`  | `IVR_INTENT_CREATE_ERROR`  | Unexpected DB error. Cause chain preserved in Sentry.                                     |

***

## Update intent

<Note>
  `PATCH /api/v1/voice/ivr-intents/{id}`
</Note>

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

```bash cURL theme={null}
curl -X PATCH "https://api.orbit.devotel.io/api/v1/voice/ivr-intents/ivri_3f1c8b2a9d4e5f7a8b6c1d2e3f4a5b6c" \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{ "active": false }'
```

**200 OK** — returns the updated row.

**404 NOT\_FOUND** — id not found in the tenant schema.

***

## Delete intent

<Note>
  `DELETE /api/v1/voice/ivr-intents/{id}`
</Note>

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

```bash cURL theme={null}
curl -X DELETE "https://api.orbit.devotel.io/api/v1/voice/ivr-intents/ivri_3f1c8b2a9d4e5f7a8b6c1d2e3f4a5b6c" \
  -H "X-API-Key: dv_live_sk_your_key_here"
```

**204 No Content** on success.

**404 NOT\_FOUND** — id not found.

***

## Test classification

<Note>
  `POST /api/v1/voice/ivr-intents/test`
</Note>

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.

<ParamField body="utterance" type="string" required>
  Caller utterance to classify. Max 500 chars (utterances beyond this are truncated by the classifier).
</ParamField>

<ParamField body="language" type="string" default="en">
  IETF BCP-47 language tag (`en`, `en-US`, `tr`, `es-419`, …). Influences classifier prompt locale.
</ParamField>

```bash cURL theme={null}
curl -X POST "https://api.orbit.devotel.io/api/v1/voice/ivr-intents/test" \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "utterance": "I would like to ask about my last invoice",
    "language": "en"
  }'
```

**200 OK**

```json theme={null}
{
  "data": {
    "matched_intent_id": "billing",
    "confidence": 0.93,
    "reasoning": "Utterance explicitly mentions 'invoice', a direct billing concept."
  },
  "meta": { "request_id": "req_abc123", "timestamp": "2026-05-24T12:00:00Z" }
}
```

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"`:

```json theme={null}
{
  "data": {
    "matched_intent_id": null,
    "confidence": 0.41,
    "reasoning": "No active intent describes a request to reset a password.",
    "fallback_reason": "no_match"
  },
  "meta": { "request_id": "req_abc123", "timestamp": "2026-05-24T12:00:00Z" }
}
```

**Performance:** P95 \< 1s (Haiku call \~250ms + intent fetch + audit log).

***

## Error model

All errors follow the platform envelope:

```json theme={null}
{
  "error": {
    "code": "IVR_INTENT_NAME_CONFLICT",
    "message": "An intent with name 'sales' already exists.",
    "status": 409
  },
  "meta": {
    "request_id": "req_abc123",
    "timestamp": "2026-05-24T12:00:00Z"
  }
}
```

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

* [Voice API overview](/api-reference/voice)
* [Voice Quickstart](/voice/quickstart)
* [Webhook events — call lifecycle](/webhooks/events)
