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

# IVR Flow JSON Reference

> Every block, field, exit and limit of an IVR flow, with the rules a flow must pass before it can be published

# IVR Flow JSON Reference

This page lists every field of an [IVR flow](/ivr-flows): the blocks, what each one says and does, its exits, the allowed values, and the rules the dashboard checks before you can publish. The editor saves your canvas in this format; each field below is what an editor setting stores, and the **In the editor** column names that setting.

A flow is a graph of blocks (`nodes`) joined by arrows (`edges`). Each arrow leaves a block through a named exit and points at another block. A block has at most one arrow per exit.

```json theme={null}
{
  "version": 1,
  "nodes": [ ... ],
  "edges": [
    { "from": "<block id>", "exit": "<exit name>", "to": "<block id>" }
  ]
}
```

`version` is always `1`. Every block has an `id` that is unique in the flow; arrows refer to blocks by `id`. An `id` is non-empty text of up to 64 characters. Use letters, digits, `_` and `-`: the editor creates ids that way, and Build with AI only accepts ids made of them. Every block except Start also has a `name`, shown as **Name** in the editor (see [Names and limits](#names-and-limits)).

> [!NOTE]
>
> **Build with AI.** If your editor shows a **Build with AI** button, its assistant can draft a whole flow or change one for you from a plain description. You review its change on the canvas and click **Apply** to keep it. A few things are always added by hand: secret API headers on Fetch blocks and recorded audio. The flow's language and voice are picked in the editor toolbar. Each message is charged in credits for what it really used, based on the model and thinking level you pick; the panel shows what a message usually costs before you send (see [Billing](/credits)).

***

## Blocks

| Block | `type` | Exits |
| - | - | - |
| [Start](#start) | `start` | `next` |
| [Say](#say) | `say` | `next` |
| [Menu](#menu), single | `menu` | one per key you connect (`0`-`9`, `*`, `#`), plus `no_input` and `invalid` |
| [Menu](#menu), collect | `menu` | `done`, `no_input`, `invalid` |
| [Fetch](#fetch) | `fetch` | `success`, `failed` |
| [End](#end) | `end` | none |

Every exit in the table must be connected. The one exception: a single menu only needs the keys you use (at least one).

### Start

Exactly one per flow. Declares the lead fields you send with each call; every pushed lead is checked against them.

```json theme={null}
{ "id": "start", "type": "start", "fields": [{ "name": "loan_id", "type": "text", "required": true }] }
```

| Field | In the editor | Allowed values |
| - | - | - |
| `fields` | **Lead fields** | Up to 30 lead fields, each `{ name, type, required }`. |
| `fields[].name` | Name | A [slug](#names-and-limits). |
| `fields[].type` | Type | One of the [field types](#field-types-and-how-theyre-spoken). |
| `fields[].required` | Required | `true` or `false`. A required field missing from a pushed lead makes the push fail with `400`. |

**Exit:** `next` (required). Lead fields are available everywhere in the flow as `{{lead.<name>}}`.

### Say

Speaks one line.

```json theme={null}
{ "id": "n1", "type": "say", "name": "greeting", "content": { "kind": "text", "text": "Hello {{lead.name}}." } }
```

| Field | In the editor | Allowed values |
| - | - | - |
| `name` | Name | A [slug](#names-and-limits). |
| `content` | **Line** | What to say. See [Content and variables](#content-and-variables). |
| `fallback` | **Fallback line** | Optional. Said instead of `content` when `content` can't be spoken, for example a variable with no usable value. |

**Exit:** `next` (required).

### Menu

Speaks a prompt and waits for keys. A menu has two modes.

| Field | In the editor | Allowed values |
| - | - | - |
| `name` | Name | A [slug](#names-and-limits). |
| `prompt` | **Prompt** | What to say before waiting. See [Content and variables](#content-and-variables). |
| `mode` | **Mode**: Single key / Collect digits | `"single"` or `"collect"`. |
| `timeoutMs` | **Wait for a key (s)** | How long to wait for a key, in whole milliseconds. Use 1000 to 30000 (1 to 30 seconds): the editor and Build with AI only set values in that range, though saving and publishing don't check it. 5000 to 8000 works well (6000 = 6 seconds, shown as 6 in the editor). |
| `retries` | **Retries** | 0 to 5. How many times to ask again before leaving through `no_input` or `invalid`. |
| `noInputLine` | **No input line** | Optional. Said before each re-ask after the caller pressed nothing. |
| `invalidLine` | **Invalid line** | Optional. Said before each re-ask after a key that isn't an option. |
| `collect` | Shown in Collect digits mode | Collect mode only: `{ minLen, maxLen, interDigitMs, mask }`. |

**Single mode.** The caller presses one key and the call follows the arrow wired to that key. In the editor, switch keys on under **Keys** and draw each key's arrow on the canvas. Connect at least one key (`0`-`9`, `*`, `#`); each connected key is an exit. `no_input` and `invalid` are both required.

**Collect mode.** The caller types several digits, for example a date of birth or a PIN.

| `collect` field | In the editor | Allowed values |
| - | - | - |
| `minLen`, `maxLen` | **Min digits**, **Max digits** | Whole numbers with 1 ≤ `minLen` ≤ `maxLen` ≤ 32. |
| `interDigitMs` | **Gap between digits (s)** | The pause that ends input, in whole milliseconds. Use 1000 to 10000 (1 to 10 seconds): the editor and Build with AI only set values in that range, though saving and publishing don't check it. 2000 to 3000 works well (3000 = 3 seconds, shown as 3 in the editor). |
| `mask` | **Mask (PINs)** | `true` hides the digits in transcripts. Use it for PINs and card numbers. |

Exits `done`, `no_input` and `invalid` are all required. After `done`, the digits are available as the variable `{{<menu name>.digits}}`. Keys are only allowed on single menus.

> [!TIP]
>
> A menu is the only block a loop may pass through. For "press 9 to repeat", connect key `9` back to the same menu.

### Fetch

Calls your HTTPS API and reads fields from its JSON answer.

```json theme={null}
{
  "id": "n2",
  "type": "fetch",
  "name": "loan",
  "method": "GET",
  "url": "https://api.example.com/loans/{{lead.loan_id}}",
  "headers": [{ "key": "Authorization", "encryptedValue": "••••••" }],
  "timeoutMs": 5000,
  "holdLine": { "kind": "text", "text": "One moment while I check that." },
  "fields": [{ "name": "amount", "path": "data.loan.amount", "type": "money", "required": true }]
}
```

| Field | In the editor | Allowed values |
| - | - | - |
| `name` | Name | A [slug](#names-and-limits), not `lead` or `call`. It becomes the variable group for the block's fields. |
| `method` | **Method** | `"GET"` or `"POST"`. |
| `url` | **URL** | Must start with `https://`, up to 2000 characters. Variables may go in the path or query only, never in the scheme, host or port. Private and internal addresses are refused. |
| `body` | **JSON body** | Optional, `POST` only. JSON as a string, up to 8000 characters. Variables are allowed; their values are JSON-escaped for you. |
| `headers` | **Headers** | Up to 20 request headers, for example an API key. Header values are entered in the editor and stored encrypted; the flow JSON never shows them in clear. |
| `timeoutMs` | **Timeout (s)** | 1000 to 10000 milliseconds (5000 = 5 seconds, shown as 5 in the editor). |
| `holdLine` | **Hold line** | Optional. Said while waiting for your API. |
| `fields` | **Response fields** | At least one and up to 30 response fields, each `{ name, path, type, required }`. |
| `fields[].name` | Name | A [slug](#names-and-limits). |
| `fields[].path` | Path | Dot path into the JSON answer, up to 256 characters; array items by index. For example `data.loan.amount` or `items.0.name`. |
| `fields[].type` | Type | One of the [field types](#field-types-and-how-theyre-spoken). |
| `fields[].required` | req | `true` or `false`. A required field that is missing from the answer makes the block fail. |

**Exits:** `success` and `failed` (both required). After `success`, the fields are available as `{{<fetch name>.<field>}}`.

> [!NOTE]
>
> A Fetch placed directly after Start runs **before the phone rings**. If it fails, the number is never dialed. Put the lookups your greeting needs there.

### End

Says an optional last line and hangs up.

```json theme={null}
{ "id": "n3", "type": "end", "name": "paid", "content": { "kind": "text", "text": "Thank you." }, "outcome": "paid" }
```

| Field | In the editor | Allowed values |
| - | - | - |
| `name` | Name | A [slug](#names-and-limits). |
| `content` | **Goodbye line** | Optional. The last line. |
| `outcome` | **Outcome** | A [slug](#names-and-limits) you receive in the `session.completed` webhook, for example `paid`, `wants_agent`, `no_response`. |

**Exits:** none.

***

## Content and variables

Everything a block says (`content`, `fallback`, `prompt`, `noInputLine`, `invalidLine`, `holdLine`) is one of:

| Form | Description |
| - | - |
| `{ "kind": "text", "text": "..." }` | A line of up to 2000 characters, spoken in the flow's voice. |
| `{ "kind": "audio", … }` | A recording. Set by the editor when you upload a recording; keep it as is. |

Text lines, Fetch URLs and Fetch bodies can carry **variables**, written `{{group.field}}` (the IVR Flows page calls them placeholders):

| Variable | Comes from |
| - | - |
| `{{call.from}}`, `{{call.to}}` | The number the call comes from and the number being called (E.164, type `phone`). Built in, available from Start on. |
| `{{call.session_id}}`, `{{call.call_id}}` | The call's `session_id` and `call_id`, the same ids your webhooks carry (type `text`). Built in, available from Start on. |
| `{{lead.<field>}}` | A lead field declared on Start. |
| `{{<fetch name>.<field>}}` | A response field of a Fetch block, after its `success` exit. |
| `{{<menu name>.digits}}` | The digits typed into a collect menu, after its `done` exit. |

A variable must be available on **every** path into the block that uses it. For example, a Fetch field can't be used on a block that is also reachable through that Fetch's `failed` exit.

***

## Field types and how they're spoken

Lead fields and Fetch response fields both have a `type`. The type decides how the value is checked and how it is spoken. For what to send with each lead, see [How to send each field type](/ivr-flows#how-to-send-each-field-type).

| Type | Value | Spoken as |
| - | - | - |
| `text` | Any text. | As written. |
| `number` | A number. | A number. |
| `money` | A number. | An amount with the currency: `1,250.50 rupees`. |
| `date` | `YYYY-MM-DD`, a real calendar date. | A day and month: `seventeenth October`. |
| `digits` | Digits only. | One by one: `4 2 4 5`. |
| `phone` | 8 to 15 digits, optional leading `+`. | Indian numbers (10 digits, or 12 starting with 91) are read as two groups of five digits; any other number is read digit by digit. |

***

## Names and limits

A **slug** is lowercase letters, digits and `_`, starting with a letter, up to 32 characters (for example `pay_menu`, `due_date`).

* Block names (every block except Start) are slugs and unique in the flow.
* Fetch and collect-menu names become variable groups, so they can't be `lead` or `call`.
* Lead field names, Fetch field names and End outcomes are slugs too.
* Outcomes don't have to be unique: several End blocks can report the same outcome, for example two ends that both report `no_response`.

| Limit | Value |
| - | - |
| Blocks per flow | 200 |
| Arrows per flow | 1000 |
| Fields per block | 30 |
| Fields in the whole flow | 200 |
| Headers per Fetch block | 20 |
| Text line | 2000 characters |
| Call time limit | 30 to 1800 seconds (30 minutes); default 300 (5 minutes). This is a flow setting, not a field of the flow JSON, and the editor has no field for it. To change it, use Build with AI if your editor shows it, or contact Vocobase. |

***

## Rules every flow must pass

The editor checks these as you build and lists every problem on the block it belongs to. A flow can't be published until all of them pass.

* Exactly one Start block.
* At least one End block.
* Every block can be reached from Start.
* From every block, some path reaches an End.
* Every required exit is connected, and no exit is connected twice.
* Keys are only connected on single menus, and a single menu has at least one key.
* Any loop passes through a Menu.
* Every Fetch block has at least one response field.
* Every variable is available on every path into the block that uses it.
* Names, fields and outcomes follow [Names and limits](#names-and-limits); Fetch URLs and timeouts follow [Fetch](#fetch).

***

## Example

A payment reminder. Start declares the lead's `name`, `amount` and `due_date`. A Say block greets the caller with their details, and a single menu offers two choices. Pressing `1` ends the call as `paid`, pressing `2` ends it as `wants_agent`, and no key or a wrong key (after two re-asks) ends it as `no_response`. The menu waits 6000 milliseconds for a key, shown as 6 seconds in the editor.

```json theme={null}
{
  "version": 1,
  "nodes": [
    {
      "id": "start",
      "type": "start",
      "fields": [
        { "name": "name", "type": "text", "required": true },
        { "name": "amount", "type": "money", "required": true },
        { "name": "due_date", "type": "date", "required": true }
      ]
    },
    {
      "id": "n_greeting",
      "type": "say",
      "name": "greeting",
      "content": { "kind": "text", "text": "Hello {{lead.name}}, this is a reminder that {{lead.amount}} is due on {{lead.due_date}}." },
      "fallback": { "kind": "text", "text": "Hello, this is a reminder about your upcoming payment." }
    },
    {
      "id": "n_pay_menu",
      "type": "menu",
      "name": "pay_menu",
      "prompt": { "kind": "text", "text": "To pay now, press 1. To talk to someone, press 2." },
      "mode": "single",
      "timeoutMs": 6000,
      "retries": 2,
      "noInputLine": { "kind": "text", "text": "Sorry, I didn't hear a key." },
      "invalidLine": { "kind": "text", "text": "Sorry, that key isn't an option." }
    },
    {
      "id": "n_paid",
      "type": "end",
      "name": "paid",
      "content": { "kind": "text", "text": "Thank you. We'll send you a payment link by SMS." },
      "outcome": "paid"
    },
    {
      "id": "n_wants_agent",
      "type": "end",
      "name": "wants_agent",
      "content": { "kind": "text", "text": "Thank you. Someone from our team will call you back shortly." },
      "outcome": "wants_agent"
    },
    {
      "id": "n_no_response",
      "type": "end",
      "name": "no_response",
      "content": { "kind": "text", "text": "We'll try you again later. Goodbye." },
      "outcome": "no_response"
    }
  ],
  "edges": [
    { "from": "start", "exit": "next", "to": "n_greeting" },
    { "from": "n_greeting", "exit": "next", "to": "n_pay_menu" },
    { "from": "n_pay_menu", "exit": "1", "to": "n_paid" },
    { "from": "n_pay_menu", "exit": "2", "to": "n_wants_agent" },
    { "from": "n_pay_menu", "exit": "no_input", "to": "n_no_response" },
    { "from": "n_pay_menu", "exit": "invalid", "to": "n_no_response" }
  ]
}
```

To push a lead to this flow, send `data` such as `{ "name": "Asha", "amount": 1250.5, "due_date": "2026-10-17" }`. The greeting is spoken as "Hello Asha, this is a reminder that 1,250.50 rupees is due on seventeenth October." See [Push a lead](/ivr-flows#push-a-lead).


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