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

# IVRs and numbers

> Manage numbers and IVRs from your application, and let your users design an IVR in FireTone's designer, opened in a popup.

Your application can create IVRs, point numbers at them, and ring people into
them. The IVR itself is **drawn in FireTone's designer**, which your
application opens in a popup. Your users need no FireTone login.

| You want to | Call |
| - | - |
| Create an IVR | `POST /ivr-flows` |
| Let a user design it | `POST /ivr-flows/{id}/designer-session`, then open the URL |
| Read what they designed | `GET /ivr-flows/{id}` |
| Send a number's calls to it | `PATCH /dids/{id}` |
| Ring someone into it | `POST /calls/ivr` |

Make the key from the **IVRs & numbers** preset (Developer → API keys).

## The whole sequence

```text theme={null}
your server                         FireTone                        your user's browser
POST /ivr-flows {name}        ───►  201 {id}
POST /ivr-flows/{id}/designer-session
     {return_origin}          ───►  201 {url, expires_at}
                                                     window.open(url) ─► the designer (a popup)
                                                     ◄─ message: ivr.published
GET  /ivr-flows/{id}          ───►  the definition
PATCH /dids/{did}             ───►  inbound calls run it
POST /calls/ivr               ───►  rings someone into it
```

## 1. Create an IVR

```bash theme={null}
curl -X POST https://api.your-host/api/v1/ivr-flows \
  -H "authorization: Bearer $FIRETONE_KEY" -H 'content-type: application/json' \
  -d '{ "name": "Appointment reminder" }'
```

The answer carries the IVR's `id`. It has an empty draft and no published
revision yet, so it answers no calls.

## 2. Open the designer

**On your server**, ask for a link. Never do this from the browser: it needs
your API key.

```bash theme={null}
curl -X POST https://api.your-host/api/v1/ivr-flows/$FLOW_ID/designer-session \
  -H "authorization: Bearer $FIRETONE_KEY" -H 'content-type: application/json' \
  -d '{ "return_origin": "https://app.example.com" }'
```

```json theme={null}
{
  "url": "https://acme.your-host/embed/ivr/9a41…#s=Zk3…",
  "flow_id": "9a41…",
  "expires_at": "2026-10-05T12:30:00Z"
}
```

* **`return_origin`** is the origin of the page that will open the popup:
  scheme, host and port, nothing else. The designer reports to this origin
  only. It must be `https` (`http://localhost` is accepted while you develop).
  A wildcard is refused.
* **`ttl_minutes`** is how long the link and the designer session last: 120 by
  default, 5 to 480.

**In the browser**, open the URL in a popup and listen for the designer's
messages:

```html theme={null}
<button id="design">Design the IVR</button>
<script>
  // Your FireTone panel's address: the origin of the `url` you were given.
  const FIRETONE = "https://acme.your-host";

  document.getElementById("design").onclick = async () => {
    // Your own endpoint, which calls POST /ivr-flows/{id}/designer-session.
    const { url } = await (await fetch("/my-app/ivr/designer-link", { method: "POST" })).json();
    window.open(url, "firetone-designer", "width=1280,height=800");
  };

  window.addEventListener("message", (event) => {
    if (event.origin !== FIRETONE) return;          // only FireTone
    if (event.data?.source !== "firetone") return;

    switch (event.data.type) {
      case "ivr.saved":      // the draft was saved
      case "ivr.published":  // a new revision answers calls
      case "ivr.closed":     // the user pressed Done or closed the window
        refreshIvr(event.data.flow_id);
    }
  });
</script>
```

Open the popup from a click, or the browser blocks it. Don't pass `noopener`:
the designer needs the window that opened it to report back.

### What the designer tells you

```json theme={null}
{
  "source": "firetone",
  "type": "ivr.published",
  "flow_id": "9a41…",
  "name": "Appointment reminder",
  "draft_revision": 4,
  "published_revision": 3
}
```

| `type` | When |
| - | - |
| `ivr.saved` | The user saved the draft |
| `ivr.published` | The draft was published: it now answers calls |
| `ivr.closed` | The user pressed **Done** or closed the window |

A message is a hint to look. **Read the IVR back with `GET /ivr-flows/{id}`**
before acting on it, and always check `event.origin`.

### What the link can do

* **It works once.** The designer trades it for a session as it loads. A
  copied, reused or reloaded link shows "This designer link has ended"; ask
  for a new one.
* **The session is for one IVR.** It can read, save and publish that IVR, and
  read the lists its steps choose from (agents, queues, extensions,
  recordings, templates). It can upload a recording. Everything else answers
  `403 designer_session_scope`.
* **It ends** at `expires_at`, when the user presses **Done**, or when the API
  key that asked for it is revoked.
* **The secret is in the URL fragment** (after `#`), which browsers never send
  to a server. Don't log the URL or store it.

## 3. Read the definition

```bash theme={null}
curl https://api.your-host/api/v1/ivr-flows/$FLOW_ID \
  -H "authorization: Bearer $FIRETONE_KEY"
```

The answer has the IVR's `published_revision` and `draft_revision`, its
`nodes` (each step: `id`, `kind`, `label`, `config`) and its `edges` (which
outlet of which step leads where). Add `?revision=draft` for the work in
progress, or `?revision=published` for what callers hear.

The definition is for reading. To change an IVR, open the designer; there is
no public call that accepts a drawing.

`POST /ivr-flows/{id}/publish` publishes the current draft from your server.
It answers `422` with a list of what is wrong when the draft can't run.

## 4. Point a number at it

```bash theme={null}
curl -X PATCH https://api.your-host/api/v1/dids/$DID_ID \
  -H "authorization: Bearer $FIRETONE_KEY" -H 'content-type: application/json' \
  -d '{ "destination_type": "flow", "destination_id": "'$FLOW_ID'" }'
```

From the next call, the number is answered by the IVR's published revision.

### Numbers

| Call | What it does |
| - | - |
| `GET /dids` | Your numbers, with where each one goes |
| `GET /dids/{id}` | One number |
| `POST /dids` | Add a number |
| `PATCH /dids/{id}` | Change where it goes, or switch it off (`enabled: false`) |
| `DELETE /dids/{id}` | Remove it |

A number goes to one destination:

| `destination_type` | Send with it |
| - | - |
| `flow` | `destination_id`: an IVR |
| `queue` | `destination_id`: a queue |
| `extension` | `destination_id`: an extension, or `destination_value`: its number |
| `conference` | `destination_id`: a room |
| `external` | `destination_value`: the number to forward to |

The destination must belong to the same organisation as the number.

## 5. Ring someone into it

```bash theme={null}
curl -X POST https://api.your-host/api/v1/calls/ivr \
  -H "authorization: Bearer $FIRETONE_KEY" -H 'content-type: application/json' \
  -H "Idempotency-Key: appt-7741" \
  -d '{
    "flow_id": "9a41…",
    "to": "+61412345678",
    "caller_id": "+61298765432",
    "variables": { "name": "Sam", "appointment": "Tuesday 3pm" },
    "reference": "appt-7741",
    "callback_url": "https://app.example.com/firetone/calls"
  }'
```

* **It answers `202` straight away**, with a `request_id`. The number is
  dialled in the background.
* **Whoever answers hears the IVR's published revision**, exactly as a caller
  to a number pointed at it would.
* **`variables`** are available to the IVR's steps as `{{name}}`: in a spoken
  prompt, an SMS, an email or a data lookup. Up to 30, of up to 500 characters
  each.
* **`caller_id`** must be one of your numbers. You can leave it out if you
  have only one.
* **Progress** goes to `callback_url` (`call.started`, `call.ended`, or
  `call.failed` with the reason) and is kept on
  `GET /call-requests/{request_id}`. A callback URL needs a
  [callback secret](/api/calls#hearing-how-it-went).

Refused straight away, with `422` and the reason:

* the IVR has never been published;
* the number is one of your own;
* no route reaches it;
* billing refuses it.

Add `?dry_run=true` to check all of that and get the plan back without
ringing anyone.


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