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

# Collect structured intake in an IVR with slot-filling

> Between a DTMF tree and full conversational AI: pair one NLU intent with required slots, prompt and reprompt until account numbers, dates and yes/no confirmations are captured and validated, bias recognition with NLU hints, and hand the filled values to downstream flow nodes.

Intent routing (see [Build an IVR flow with NLU intent routing](/guides/ivr-routing-nlu)) answers one question: *what does the caller want?* Most self-service flows need a second answer: *the details.* This guide walks the middle path between a press-key tree and a full conversational AI — one intent, a short list of slots collected from free-form speech, validated deterministically, and posted to your flow as variables. The worked example is a "report outage" intake that collects a service address, a 10-digit account number, the outage start date, and a callback confirmation.

## 1. Intent vs slot: what slot-filling adds

An intent routes the call; a slot fills in the record. With intent-only routing, "I want to report an outage at 123 Main Street" lands in the right queue and every detail is re-asked by a human. With a DTMF tree, each detail costs a keypad branch — fine for a 4-digit PIN, unusable for a street address. Slot-filling sits between them:

* **One intent, many values.** The caller's utterance is mined for several structured values at once — account number, date, amount, address — instead of one regex per prompt that you write and maintain yourself.
* **Validation in the loop, not after it.** Each candidate value is checked against its slot type (digit count, calendar validity, closed value set) before it is accepted. A hallucinated or mis-heard value never lands in your record; the caller is reprompted with the reason instead.
* **Relative speech becomes absolute data.** "It started yesterday" is resolved to a full calendar date; "\$1,234.50" and "1234.5" normalise to the same amount.

Reach for slot-filling when the intake is a fixed set of fields you already store somewhere (ticket systems, order lookups, compliance forms). Reach for a full conversational AI agent when callers negotiate, change topic mid-flow, or need open-ended help — and for a two-level menu, keep DTMF for the first split and slot-filling for the details.

## 2. Define an intent and its required slots

Two surfaces, one flow. The **intent** is created on the IVR intents endpoint (`POST /api/v1/voice/ivr-intents`) exactly as in the [routing guide](/guides/ivr-routing-nlu) — a slug, a rich description, and a destination:

```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": "report_outage",
    "description": "Caller reports a service outage or interruption at their address and needs a technician or status update.",
    "target_queue_id": "que_outage"
  }'
```

The **slot spec** is the list of fields the intake collects. It is passed by value on every extraction turn to `POST /api/v1/voice/ivr-slots/extract`, so there is nothing to migrate or pre-provision — your flow threads the spec and the accumulating values through each turn:

```json theme={null}
{
  "slots": [
    {
      "name": "service_address",
      "type": "string",
      "prompt": "What is the street address of the outage?",
      "description": "House number and street name, e.g. 123 Main Street",
      "maxLength": 120
    },
    {
      "name": "account_number",
      "type": "account_number",
      "prompt": "Please say or key your 10-digit account number.",
      "reprompt": "Please repeat the 10-digit account number, one digit at a time if easier.",
      "minLength": 10,
      "maxLength": 10
    },
    {
      "name": "ticket_date",
      "type": "date",
      "prompt": "On which date did the outage start?",
      "description": "Resolve relative phrases like yesterday or last Friday to a full date"
    },
    {
      "name": "callback_ok",
      "type": "string",
      "prompt": "May we call you back to confirm the repair? Say yes or no.",
      "enumValues": ["yes", "no"]
    }
  ]
}
```

Field rules, from the extraction contract:

| Field | Effect |
| - | - |
| `name` | Lowercase alphanumeric plus `-` / `_`, max 64 chars. It keys the slot in every response and becomes the variable name downstream (section 6). Duplicate names in one turn are rejected with `422`. |
| `type` | One of `account_number`, `order_id`, `date`, `amount`, `phone`, `email`, `zip_code`, `number`, `string`. Selects the deterministic validator. |
| `prompt` | Played (as TTS) when the slot is the next outstanding one. Max 500 chars. |
| `reprompt` | Played instead of `prompt` when the slot is missing or invalid on a later turn. Falls back to `prompt` when omitted. |
| `description` | A hint to the extractor about what a valid value looks like — the place for grammar notes like "house number and street name". |
| `required` | Default `true`. Optional slots are collected when offered but never trigger a reprompt or block `complete`. |
| `minLength` / `maxLength` | Length bounds after normalisation, for `string`, `account_number` and `order_id`. `account_number` defaults to 4–20 digits when unset. |
| `enumValues` | A closed value set, matched case-insensitively. The accepted value is normalised to the casing you list — safer than any free-text field for yes/no, plan tier, or reason codes. |

A few validators worth designing prompts around: `date` accepts only an absolute calendar date (the extractor resolves "yesterday" for you, but an unresolvable "sometime last week" returns null and the slot stays open), `amount` normalises to at most two decimal places, and `account_number` strips the pauses STT inserts between spoken digits. Extraction runs one turn per request — send `{ "utterance", "slots", "filled", "language" }` and receive per-slot status, the merged `filled` map, the next reprompt to play, and `complete: true` once every required slot holds a valid value.

## 3. Prompt, confirm, reprompt

The extract endpoint is stateless by design: it validates one utterance and hands back the accumulated `filled` map. **Your flow owns the loop and the attempt counter.** The turn shape:

1. Play `next_prompt.message` (or your opening prompt on the first turn).
2. Capture the caller's speech and call `/ivr-slots/extract` with the prior `filled` map.
3. If a slot came back `invalid`, its reprompt is prefixed with the rejection reason ("Account number is too short (need at least 10). Please repeat…") — play it verbatim; it tells the caller *why*.
4. Loop until `complete` is true or your retry budget is exhausted.

**Max-retry policy.** Because the endpoint never counts attempts, cap them in the flow: two reprompts per slot is a good default; three for digit-heavy slots with a DTMF fallback edge. When the budget is exhausted, take the escalation edge — route to the intent's `target_queue_id` or `target_agent_id` with whatever slots did fill attached, so the agent never re-asks for values you already hold. Escalating with partial slots is almost always better than escalating empty-handed.

**Confirmation turns.** For values that are expensive to get wrong, confirm by reading the *normalised* value back and capturing the answer as a yes/no enum slot — that is what `callback_ok` above models, and the same shape works for confirming an account number: "I have account number ending 4 0 2 1 — is that correct?" A "no" means you delete that key from `filled` before the next extract call so the slot reopens; the validator never re-accepts a value you keep passing in as already filled. Keep confirmations to one turn each and only for slots where a mistake has a real cost — every extra confirmation turn is measured abandonment.

## 4. Bias recognition with NLU hints

Numerals and addresses are where speech recognisers stumble, because your callers' vocabulary is not the recogniser's default vocabulary. The NLU hints catalogue (`/api/v1/voice/ivr-nlu-hints`) is where you seed example phrasings per intent; `POST /api/v1/voice/ivr-nlu-hints/build-hints` flattens the catalogue into the deduplicated, normalised hint list a `speechInput` node's recogniser consumes — call it to preview exactly what the recogniser will see.

Hinting the 10-digit account number and the street-address grammar for our example:

```bash cURL theme={null}
curl -X POST "https://api.orbit.devotel.io/api/v1/voice/ivr-nlu-hints" \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "outage_account_number",
    "description": "How callers dictate their 10-digit account number",
    "utterances": [
      { "text": "my account number is four zero zero one two three four five six seven" },
      { "text": "account four zero zero one two three four five six seven" },
      { "text": "it is on my bill, ten digits starting with four zero zero" }
    ]
  }'
```

```bash cURL theme={null}
curl -X POST "https://api.orbit.devotel.io/api/v1/voice/ivr-nlu-hints" \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "outage_service_address",
    "description": "Street-address grammar used in this region",
    "utterances": [
      { "text": "123 main street" },
      { "text": "45 oak avenue apartment 2b" },
      { "text": "1200 industrial parkway building c" }
    ]
  }'
```

Rules of the catalogue, so hints behave as expected:

* Hints are **example phrasings, not regexes** — write the sentences callers actually say, including the filler words ("my account number is…") the recogniser should look past.
* Every utterance is normalised (lowercased, punctuation folded to spaces) and deduplicated; the same phrase in two intents is kept once, first intent wins. `build-hints` reports a `dropped` count for unusable entries and a per-intent breakdown so you can audit what survived.
* Limits: 64 intents per catalogue, 16 utterances per intent, 100 characters per utterance, 256 hints returned per build. Spend them on the slots that actually mishear — digits and addresses — not on greetings.
* Hints bias recognition; they do not validate. A hinted phrase that misfires still has to pass the slot's deterministic validator (section 7), so hints widen what the recogniser hears while `minLength` / `maxLength` / `enumValues` keep the accepted set narrow.

## 5. Test collection end to end

Before the intent touches a live route, run the same harness you would for routing, extended down to the slots:

1. **Routing first.** `POST /api/v1/voice/ivr-intents/test` with your fixture utterances — "the power is out on my street", "no service since this morning" — and confirm `report_outage` wins at confidence ≥ 0.7 against your other active intents, per the [routing guide's threshold contract](/guides/ivr-routing-nlu#4-test-utterances-before-going-live).
2. **Turn-by-turn extraction.** Script `/ivr-slots/extract` calls the way the flow will: first turn with an empty `filled`, each following turn passing the returned `filled` map plus the next utterance. Assert the normalised values (`account_number` is digits-only and exactly 10 long, `ticket_date` is ISO `YYYY-MM-DD`), assert an invalid turn returns `status: "invalid"` with a reason and a reprompt, and assert `complete` flips to true only when all four required slots hold.
3. **The unhappy paths.** Feed a 9-digit account number, a "February 30th" date, and a mumbled "maybe" against the yes/no slot — each must reject with a reprompt, never silently fill.
4. **The published graph.** Run the flow through the simulator as in [Build your first IVR flow](/guides/build-ivr-flow): the escalation edge on retry exhaustion, the fallback edge when the intent never matches, and the happy path landing on the downstream node from section 6. Only then attach the intent to the live call route.

Every extract and test call writes an audit entry and an outcome counter (`complete` vs `incomplete`), so your fixture runs leave a trail you can diff against live traffic once the intake ships.

## 6. Where the filled slots land

When the collection loop completes, each filled slot is posted to the flow as a variable named `slot_<name>` and exposed in the conversation context, using the same `{{…}}` templating the [secure payment capture guide](/guides/ivr-secure-payment-capture) uses for captured values. A downstream node consumes them directly — for example, a playAudio node that confirms the logged ticket:

```json theme={null}
{
  "id": "confirm_ticket",
  "type": "playAudio",
  "data": {
    "ttsText": "Thank you. I have logged an outage at {{slot_service_address}} starting {{slot_ticket_date}}. An agent will call you back to confirm the repair."
  }
}
```

and an HTTP/webhook node that creates the ticket with the same variables in its request body, so your ticketing system receives `service_address`, `account_number`, `ticket_date` and `callback_ok` already normalised — digits joined, date absolute, yes/no in your casing — with no parsing of raw transcripts on your side. Where the next step depends on a value, branch by edge rather than by expression: two edges out of the completing node, one taken when `slot_callback_ok` is `yes`, mirror how a `speechInput` node branches on intent slugs in the [routing guide](/guides/ivr-routing-nlu#6-apply-the-intents-in-an-ivr-flow).

## 7. Failure modes

**Slot never fills (bad hint grammar).** If the recogniser never hears what the extractor can find, the slot cycles `missing` → reprompt until your retry budget escalates. Check `build-hints` first: a high `dropped` count means utterances exceeded the 100-character cap or normalised to nothing, and punctuation-heavy hints ("123 Main St.") arrive folded to spaces — write hints the way speech sounds. Then check the extract turn's `reasoning`: it tells you whether the extractor saw nothing in the utterance or saw a value the validator rejected.

**Hints match too broadly (false accepts).** A hint list seeded with short fragments ("yes", "4 0 0") makes the recogniser hear slot values in unrelated speech. The defense is the validator, not the hint: pin `account_number` to `minLength: 10, maxLength: 10`, keep `enumValues` closed, and treat `description` on the slot as the narrow grammar. A false accept then surfaces as a one-turn `invalid` reprompt instead of a corrupted record — and the confirmation turn (section 3) is the last line of defense for the values that matter.

**Confirm-loop abandonment.** Every confirmation and reprompt turn costs callers; long digit dictation on a mobile keypad is where most hang up. Keep required slots to what the downstream system truly needs, mark nice-to-have fields `required: false` so they never block `complete`, offer the DTMF fallback edge for digit-heavy slots, and always keep the escalation edge to a staffed queue or AI agent reachable when retries exhaust — with the partial `filled` map attached.

<Warning>
  Slot-filling captures values such as phone numbers as data only. Any outbound call your flow places afterwards — a callback, a transfer, a notification — must route through your configured Orbit voice routing; the intake itself never dials.
</Warning>

## See also

* [Build an IVR flow with NLU intent routing](/guides/ivr-routing-nlu)
* [IVR intents endpoint reference](/voice/ivr-intents)
* [Build your first IVR flow](/guides/build-ivr-flow)
* [Capture payments securely with IVR](/guides/ivr-secure-payment-capture)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.