# IVR Extensions Management API

A complete guide to the IVR (phone-tree) extensions management API. This document is self-contained — you do not need prior knowledge of the system. By the end you will know how to build a UI, a mobile app, or a script that can fully manage extensions, files, permissions and system messages.

---

## 1. Mental Model

Before any endpoint, understand the five concepts the whole API is built around:

### 1.1 Extension
An **extension** is a single node in a phone tree. Every extension has:
- An **ID** (internal, numeric).
- A **`type`** — what it does when a caller reaches it (e.g. `menu`, `simpleRouting`, `audioPlayer`, `record`). The full list is provided by the API (see `getExtensionTypes`).
- A **`name`** — display name in the management UI.
- An **`extension`** — the digit(s) a caller must press in the parent menu to reach this extension (e.g. `"2"`).
- A **`belowExtension`** — the ID of its parent. The root extension has `belowExtension = null`.
- **`settings`** — a JSON bag of type-specific fields (e.g. `dialPhone`, `voiceMail`, `menuReplay`). The available settings per type are described by `forms` + `fields` (see Schema section).
- **`security`** — a JSON list of access rules (e.g. "only these phones can reach this extension", "require password").

### 1.2 File
Extensions can hold **audio files** (played by the `audioPlayer` type, saved by the `record` type, etc.). A file has:
- An **ID**.
- A **`name`** (display).
- An **`ivrName`** (internal filename used for playback).
- A **`belowExtension`** — which extension owns the file.
- A **`runningOrder`** — playback order inside the extension (editable).
- **`taggs`** — freeform tags, used e.g. for cross-extension playback by tag.

### 1.3 Tree
Extensions form a **tree**: each one has one parent (`belowExtension`) and can have many children. The root is the "main menu". The whole tree for the logged-in account is returned by a single call (`foldersList`).

### 1.4 Schema (`types`, `fields`, `forms`)
Because different extension types expose very different settings, the API ships the UI schema itself:
- **`types`** — every available extension type (key + display name + icon).
- **`fields`** — every setting field that can appear in any form (key + input type + options + label + tooltip).
- **`forms`** — for each type, the ordered layout of fields to render (with conditional visibility rules).

This means a UI does not need to hard-code forms. It fetches the schema, then renders each selected type dynamically.

### 1.5 System Messages
Every extension type has built-in voice prompts ("please press 1", "invalid input"). An account can override any of these with a custom recording or a TTS text, globally or per-extension.

---

## 2. Authentication

Every request must include an API key:

```
apiKey=YOUR_API_KEY
```

Send it as a query parameter (GET) or a form field (POST). Your API key is issued by customer service — contact them to obtain it.

---

## 3. Base URL and transport

```
https://api.tlivr.com/ivrFilesApi.php
```

- Every call selects an operation with the `action` parameter.
- Reads use `GET`, writes typically use `POST` (`application/x-www-form-urlencoded`).
- `saveCustomMessagesBeta` expects a JSON body.
- `uploadFile` and `uploadChunk` expect `multipart/form-data`.
- All responses are JSON (unless documented otherwise).

A response that succeeded will generally include `"status": "OK"`. A failure will include `"status": "ERROR"` and a `note` (human-readable) or `errorCode`.

### 3.1 Addressing an extension: `extensionId` **or** `path`

Every action that takes an `extensionId` also accepts a **`path`** — an **absolute** path of *dialed digits* from the root menu down to the target. A path **must start with `/`** (like a filesystem path): `/` is the root, `/1` is the child you reach by dialing `1`, `/1/2/3` walks three levels down. This lets you address extensions by their menu position without first looking up numeric IDs.

- **Must start with `/`.** A value that does not begin with `/` (e.g. `1/2/3`) is **not a valid path** and is rejected. `/` alone = the **root** menu.
- **Separator-tolerant / smart.** Beyond the required leading `/`, extra or duplicate slashes and surrounding spaces are ignored: `/1/2/3`, `/1//2///3/`, and ` /1 / 2 /3 ` all resolve to the same node. `\` works as a separator too.
- **Precedence.** If you send both, `extensionId` wins; `path` is used only when `extensionId` is absent.
- **Not found / invalid.** An unresolvable or badly-formatted `path` makes the action fail with the normal "not your extension" error (`errorCode 5`) — it never silently targets the wrong node.
- **Digit-less children.** A *non-root* node is reachable by `path` only if it has a dialed digit (`extension`). Some children (e.g. under an `api`-type menu) have an empty `extension` and therefore **cannot** be path-addressed — there is no digit to form their path segment. Use their numeric `extensionId` instead (from `foldersList`).
- **Copy/move targets.** The destination of `folderCopy` / `folderMove` / `fileCopy` / `fileMove` (`target`) likewise accepts **`targetPath`** (same `/`-prefixed format).

```
# these two are equivalent:
GET ivrFilesApi.php?action=filesList&extensionId=60412&apiKey=...
GET ivrFilesApi.php?action=filesList&path=/1/2/3&apiKey=...

# the root menu:
GET ivrFilesApi.php?action=filesList&path=/&apiKey=...

# upload to the extension reached by dialing 1 then 4 (extra slashes are fine, leading / is required):
POST ivrFilesApi.php?action=uploadFile&path=/1//4&apiKey=...   (multipart: file=...)
```

---

## 4. Quick-start: the ten-minute UI

If you are building a new management interface from scratch, this is the minimum flow:

1. **Boot once** — call `getUiSchema`. Cache `types`, `fields`, `forms`, `accountSettings`, `mailingLists`.
2. **Load the tree** — call `foldersList`. Render it.
3. **On node click** — call `folderSettings?extensionId=<id>` to get its `settings` + `security` + `type`.
4. **Render the form** — for the node's `type`, iterate `forms[type]`. For each entry, look up the field in `fields[input]` and render the right input widget (respecting `view_if`, `startLine`, `size`).
5. **For dropdowns with `"options": "GET"`** — call `getOptions2?list=<optionsKey>` and fill the `<select>` / `<Select2>`.
6. **Save** — POST `extensionSet` with `extensionId`, `type`, `name`, `extension`, and the user-entered values under `settings[*]`.
7. **Files tab** — `filesList?extensionId=<id>` and render a table with rename / tag / reorder / delete actions.
8. **Upload** — `POST uploadFile` with `multipart/form-data`.
9. **Permissions tab** — POST `securitySet` with one or more `S1[...]`, `S2[...]` blocks.

That is the entire core loop.

---

## 5. Rendering Rules (critical for UIs)

### 5.1 A field (`fields[key]`)

| Property       | Meaning                                                                                 |
|----------------|-----------------------------------------------------------------------------------------|
| `id`           | HTML `name` attribute for the input.                                                    |
| `type`         | `text` / `number` / `email` / `url` / `datetime-local` / `select` / `multySelect` / `selectTags` / `audio`. |
| `name`         | Display label shown above the input.                                                    |
| `tolip`        | Hover tooltip text.                                                                     |
| `explanation`  | Help text shown below the input.                                                        |
| `default`      | Default value.                                                                          |
| `options`      | For `select` / `multySelect`: either **hardcoded** `"val1~txt1*val2~txt2"` **or** the literal string `"GET"`. |
| `optionsKey`   | When `options === "GET"`, the key to send to `getOptions2?list=<key>`.                  |

### 5.2 A form entry (`forms[type][i]`)

| Property                   | Meaning                                                                                   |
|----------------------------|-------------------------------------------------------------------------------------------|
| `type`                     | `input` (render a field) or `html` (inject a literal HTML block).                         |
| `input`                    | Key into `fields`.                                                                        |
| `input_id`                 | Override the rendered `id` — useful when the same field is rendered multiple times (e.g. `digits1`, `digits2`, … for each keypad digit). |
| `title`                    | Override the label for this instance only.                                                |
| `view_if`                  | Conditional visibility (see below).                                                       |
| `startLine`                | `"yes"` forces a new grid row.                                                            |
| `size`                     | `1` = full-width (spans 2 grid columns), `2` = half-width.                                |
| `default`                  | Default value for this instance.                                                          |
| `errorExit`                | If the dropdown (`options: GET`) has no options for the user, show this message in place of the form. |
| `html`                     | Literal HTML (only when `type === "html"`).                                               |
| `audioPlayerTemplateStart` | Internal marker for slicing out the reusable `audioPlayerTemplate` subset.                |

### 5.3 `view_if` syntax

A small DSL for conditional fields:

```
fieldId:value                    → show when field == value
fieldId:val1*val2                → show when field is val1 OR val2 (same field)
condA & condB                    → AND (all must hold)
condA | condB                    → OR (any holds)
```

Example: `"passType:list*manuelPass & adminLogin0:yes"` — show when `passType` is either `list` or `manuelPass` AND `adminLogin0` is `yes`.

### 5.4 Dropdowns that need remote data

When a field has `"options": "GET"` plus an `"optionsKey"`, you must resolve it with **`getOptions2?list=<optionsKey>`** (you can request several keys in one call: `list=a,b,c`). The response is `{ "<key>": [{ "value", "label" }, ...], ... }`.

---

## 6. Endpoint Reference

All endpoints live at `ivrFilesApi.php?action=<name>` and require `apiKey`.

### 6.1 Schema / bootstrap

#### `getUiSchema`
Returns everything needed to render the UI in one call. **Call once on boot.**

**Request:** `GET ivrFilesApi.php?action=getUiSchema&apiKey=...`

**Response:**
```json
{
  "status": "OK",
  "types": {
    "menu":          { "name": "Menu",        "txt": "...", "icn": "78292" },
    "simpleRouting": { "name": "Call Routing","txt": "...", "icn": "53439" }
  },
  "fields": {
    "dialPhone": { "id": "dialPhone", "type": "number", "name": "Phone", "explanation": "..." },
    "voiceMail": { "id": "voiceMail", "type": "select", "options": "no~Disabled*yes~Enabled", "name": "Voicemail" }
  },
  "forms": {
    "simpleRouting": [
      { "type": "input", "input": "dialPhone", "size": 2 },
      { "type": "input", "input": "voiceMail", "size": 1 },
      { "type": "input", "input": "stt_email", "view_if": "voiceMail:yes", "size": 1 }
    ]
  },
  "accountSettings": { "use_rh": "yes", "rh_max": 5000 },
  "mailingLists":    [ { "id": "1000", "name": "General", "label": "1000-General" } ]
}
```

#### `getExtensionTypes`
Only `types` + `accountSettings`. Use for a lightweight "pick a type" dropdown.

#### `getFieldsSchema`
Only `fields`.

#### `getFormLayout`
`?type=<typeKey>` → that type's form. No `type` → all forms.

#### `getAccountSettings`
Account-level flags that affect which types to show:

| Flag             | Meaning                                                                    |
|------------------|----------------------------------------------------------------------------|
| `use_rh`         | `"yes"` enables the types `runRingHangup`, `rhAdd`, `rhRemove`.            |
| `rh_max`         | Max phone numbers allowed in a ring-hangup broadcast list. `0` = no limit. |
| `ivrSystemPhone` | The system's inbound phone number.                                         |

#### `getMailingLists`
Returns `[{ id, name, label }, ...]` — clean JSON for dropdowns.

> **Managing the contacts inside a list** (add / update / delete / block a phone, or list a list's members with search + paging) is a **separate endpoint** — see the **[Mailing Lists API](mailingListsApiDocs.html)** (`mailingListsApi.php`: `phoneEdit`, `phonesList`, `phonesDelete`, `setStatus`, `uplodePhones`). Use the `id` from `getMailingLists` as the `mailingList` parameter there. Those operations act on the same contacts shown in the system UI and are scoped to your account.

#### `getExtensionPath`
`?extensionId=<id>` → breadcrumb from root to that extension, useful for the header.

```json
{
  "status": "OK",
  "path": [
    { "id": "1",     "name": "Main Menu", "extension": "",  "type": "menu" },
    { "id": "4500",  "name": "Service",   "extension": "2", "type": "menu" },
    { "id": "12345", "name": "Agent",     "extension": "3", "type": "simpleRouting" }
  ]
}
```

---

### 6.2 Tree

#### `foldersList`
Returns the entire tree.

**Request:** `GET ivrFilesApi.php?action=foldersList&apiKey=...`

**Response** (recursive):
```json
{
  "1": {
    "id": "1",
    "name": "Main Menu",
    "belowExtension": "",
    "extension": "",
    "type": "menu",
    "children": {
      "4500": { "id": "4500", "name": "Service", "extension": "2", "type": "menu", "children": { ... } }
    }
  }
}
```

#### `folderCopy`
Recursively copies an extension (including all children and files).

| Param         | Description                                                      |
|---------------|------------------------------------------------------------------|
| `extensionId` | Source ID, or several IDs comma-separated. *(or `path`)*          |
| `target`      | Destination parent ID. *(or `targetPath`)*                       |

Returns `{ status, newExtensionIds: [..], foldersList: {..} }`. **Blocks copying into self or descendants.**

**Bulk example** — copy three extensions into target `4502`:
```
GET ivrFilesApi.php?action=folderCopy&extensionId=4001,4023,4099&target=4502
```
Each source is validated independently; invalid sources are skipped silently. The response's `newExtensionIds` is in the same order as the request, omitting any that were skipped.

#### `folderMove`
Moves an extension to a new parent (updates `belowExtension` only — children follow).

| Param         | Description                             |
|---------------|-----------------------------------------|
| `extensionId` | Source ID(s) comma-separated. *(or `path`)* |
| `target`      | Destination parent ID. *(or `targetPath`)*  |

**Bulk example**:
```
GET ivrFilesApi.php?action=folderMove&extensionId=4001,4023,4099&target=4502
```

#### `folderDelete`
Deletes an extension; files under it are soft-deleted.

| Param         | Description                              |
|---------------|------------------------------------------|
| `extensionId` | ID(s) comma-separated. *(or `path`)*     |

The root extension cannot be deleted.

---

### 6.3 Extension details

#### `folderSettings`
Full record of one extension — including parsed `settings` and `security`.

**Request:** `GET ivrFilesApi.php?action=folderSettings&extensionId=4500&apiKey=...`

**Response:**
```json
{
  "id": "4500",
  "name": "Service",
  "extension": "2",
  "belowExtension": "1",
  "type": "menu",
  "settings": { "maxDigits": "1", "menuReplay": "4" },
  "security": { "S1": { "securityType": "login", "passType": "adminPass" } }
}
```

#### `extensionSet`
Create a new extension or update an existing one.

**Request (update):**
```
POST ivrFilesApi.php?action=extensionSet&extensionId=4500&apiKey=...
Content-Type: application/x-www-form-urlencoded

name=Service&extension=2&type=menu&settings[maxDigits]=1&settings[menuReplay]=4
```

**Request (create):** set `extensionId=NEW` and provide `belowExtension=<parentId>`.

Rules:
- `type` is required.
- `settings[*]` keys must match `fields` keys that appear in `forms[type]`.
- For `menu` type with repeatable menu items, send `settings[menuItems][i][menuItem]=upload|file|text` + the appropriate per-row field (`uploadAudio`, `filesList`, or `text`).
- Uploaded audio (`audio` type field) should be a filename inside `reports/` placed there by a prior upload flow (see §6.5).

**Response:** `{ status, foldersList }`.

#### `securitySet`
Saves the extension's access rules.

Each rule is sent as `S{n}[fieldName]=value`. Any block that contains a non-empty `securityType` is persisted in order as `S1, S2, ...`.

**Example:**
```
POST ivrFilesApi.php?action=securitySet&extensionId=4500&apiKey=...

S1[securityType]=login&S1[passType]=adminPass
S2[securityType]=phoneFilter&S2[phones]=0521234567,0501234567
```

---

### 6.4 Files

#### `filesList`
`?extensionId=<id>` → ordered array of files.

```json
[
  {
    "id": "60383",
    "name": "Greeting",
    "ivrName": "abc123",
    "time": "1713000000",
    "runningOrder": 1000,
    "runningOrderView": 1,
    "details": { "phone": "+972501234567" },
    "taggs": [ { "tag": "5", "name": "greetings", "color": "#4caf50" } ]
  }
]
```

#### `uploadFile`
Upload a single audio file to an extension. `POST multipart/form-data` with `file` plus:

| Param            | Description                                                                  |
|------------------|------------------------------------------------------------------------------|
| `extensionId`    | Target extension ID (must belong to your account). *(or `path`)*            |
| `file`           | The audio file (multipart field).                                           |
| `name`           | Display name (defaults to the filename without extension). Sanitized: control characters and path separators are stripped, capped at 120 chars. |
| `checkDuplicate` | `STOP` (error if a file with the same name exists), `BACKUP` (keep old with a timestamp suffix), default (replace). |

**Security & validation.** Every uploaded file passes the same gate:
- **Size** — app cap **10 MB** for single upload, *and* the file must fit the server's per-request limit (`upload_max_filesize` / `post_max_size`). On the current server that limit is ~2 MB, so **anything bigger must use chunked upload below** — the API returns `errorCode 9` with `reason: upload_max_filesize` (or `post_max_size`) and a `maxRequestBytes` hint when a single request is too large.
- **Extension** — must be a supported audio type (`wav, mp3, ogg, oga, opus, m4a, m4b, aac, flac, amr, wma, aiff, aif, aifc, mp4, 3gp, 3gpp, webm, mka, caf, au, ac3`).
- **Real content** — the file is probed with `ffprobe` and must contain a decodable audio stream; a non-audio file renamed to `.mp3` is rejected. Maximum audio length **6 hours**.

Temporary files live **outside the web root** (a private `0700` dir) and are deleted the instant the opus is on R2 — nothing lingers on the server.

After validation the file is transcoded to opus (24 kbps / 8 kHz mono), stored on R2, and added to the extension. Response: `{"status":"OK","name":"<display name>","fileId":<id>}`.

Error codes: `2` no file, `3` invalid/undecodable audio, `4` missing `extensionId`, `5` not your extension, `6` upload/encode/storage error, `8` duplicate with `STOP`, `9` file too big, `10` unsupported file type, `11` audio too long. Failures also carry a machine-readable `reason`.

#### Chunked upload (large files): `uploadInit` → `uploadChunk` → `uploadComplete`

For files above the single-shot limit (and to survive flaky connections), upload in parts. Each part is a small `multipart/form-data` request, so you stay **under the server's per-request limit** — this is exactly what lets a 2 MB-capped server accept arbitrarily large files. The whole flow is bound to your `apiKey`: the `uploadId` is tied to your account and is unusable by anyone else. Limits: total file **≤ 512 MB**, session lives **24 h**, and **chunk size is auto-capped to what the server accepts** — `uploadInit` returns `chunkSize` (use exactly that) and `maxRequestBytes` (never send a part larger than this). Temp parts are stored outside the web root and removed after assembly.

**Step 1 — `uploadInit`** (`POST`/`GET`). Opens a session.

| Param            | Description                                                                  |
|------------------|------------------------------------------------------------------------------|
| `extensionId`    | Target extension ID (must belong to your account). **Required** *(or `path`)*. |
| `fileName`       | Original filename including extension — used for the type whitelist. **Required.** |
| `totalSize`      | Full file size in bytes. **Required.**                                       |
| `name`           | Display name (optional; defaults to `fileName` without extension).          |
| `chunkSize`      | Desired chunk size in bytes (optional). Auto-capped to the server's per-request limit; default 1 MB. Always honor the `chunkSize` returned by `uploadInit`. |
| `sha256`         | Hex SHA-256 of the whole file (optional). If sent, verified at completion.  |
| `checkDuplicate` | Same semantics as `uploadFile`.                                             |

Response: `{"status":"OK","uploadId":"<64-hex>","chunkSize":<bytes>,"totalChunks":<n>,"expiresAt":<unix>,"received":[]}`. Slice the file into `totalChunks` parts of `chunkSize` bytes (the last part is the remainder).

**Step 2 — `uploadChunk`** (`POST multipart/form-data`). Send each part. Order does not matter and a part may be re-sent safely (idempotent), which makes resume trivial.

| Param         | Description                                              |
|---------------|----------------------------------------------------------|
| `uploadId`    | From `uploadInit`. **Required.**                         |
| `chunkIndex`  | 0-based index of this part. **Required.**                |
| `chunk`       | The raw bytes of this part (multipart field). **Required.** |
| `chunkSha256` | Hex SHA-256 of this part (optional; verified if sent).   |

Response: `{"status":"OK","received":<count>,"total":<n>,"complete":<bool>}`.

**Step 3 — `uploadComplete`** (`POST`/`GET`). Reassembles, verifies (size + optional SHA-256 + `ffprobe`), transcodes to opus, stores on R2, and adds the file to the extension. The session and its parts are then deleted.

| Param      | Description                      |
|------------|----------------------------------|
| `uploadId` | From `uploadInit`. **Required.** |

Response: `{"status":"OK","name":"<display name>","fileId":<id>}`.

**Helpers.**
- `uploadStatus?uploadId=…` → `{"status":"OK","received":[…indices…],"receivedCount":n,"totalChunks":n,"expiresAt":unix}` — call this to know which parts to resend after a disconnect.
- `uploadAbort` (`uploadId`) → discards the session and all parts.

**Chunked error codes:** `5` not your extension, `9` total size missing/too big, `10` unsupported type, `11` audio too long, `12` too many chunks, `20` unknown/foreign session, `21` session not open, `22` session expired, `23` no chunk received, `24` chunk index out of range, `25` bad chunk size, `26` total size exceeded, `27` chunk hash mismatch, `28` failed to store chunk, `30` missing chunks (response includes `missing[]`), `31` assembly failed, `32` size mismatch, `33` checksum mismatch.

**Minimal flow (pseudo-code):**
```text
init = POST uploadInit  extensionId=4500 fileName=promo.wav totalSize=73400320
for i in 0 .. init.totalChunks-1:
    POST uploadChunk  uploadId=init.uploadId chunkIndex=i chunk=<bytes[i*chunkSize : (i+1)*chunkSize]>
done = POST uploadComplete  uploadId=init.uploadId      # -> fileId
```

#### `fileDelete`
Soft delete (`trash=<now>`). Accepts multiple IDs comma-separated.

| Param             | Description                                |
|-------------------|--------------------------------------------|
| `fileId`          | ID(s), comma-separated.                    |
| `belowExtension`  | Extension ID, so an updated file list can be returned. |

#### `fileRename`
| Param             | Description                 |
|-------------------|-----------------------------|
| `fileId`          | File ID.                    |
| `newName`         | New display name.           |
| `belowExtension`  | Extension ID (for refresh). |

#### `fileTagging`
| Param         | Description                                                                            |
|---------------|----------------------------------------------------------------------------------------|
| `fileId`      | File ID.                                                                               |
| `extensionId` | Extension ID. *(or `path`)*                                                            |
| `type`        | `SAVE`.                                                                                |
| `tags`        | Comma-separated tag IDs, or free text (unknown text becomes a new tag automatically). |

#### `fileOrderSet`
Reorder a file inside its extension.

| Param          | Description                                                                |
|----------------|----------------------------------------------------------------------------|
| `fileId`       | File ID.                                                                   |
| `extensionId`  | Extension ID. *(or `path`)*                                               |
| `type`         | `END` / `TOP` / `AFTER` / `RAPLACE`.                                       |
| `val`          | For `AFTER` or `RAPLACE`: the reference file's **display number** (`runningOrderView`). |

#### `fileCopy` / `fileMove`
Copy (or move — copy + trash original) files to another extension. `fileMove` is the "cut → paste" semantics.

| Param       | Description                       |
|-------------|-----------------------------------|
| `fileId`    | ID(s) comma-separated.            |
| `target`    | Destination extension ID. *(or `targetPath`)* |
| `placement` | Optional. `top` (default) — the pasted message jumps to the top of the destination list (fresh `runningOrder` = last-RO + 1000, `time` = now). `original` — keeps the source recording time and slots the file into the destination by that time, so "by file number" order still reflects recording order. |

**Bulk example** — copy four files to extension `4502`:
```
GET ivrFilesApi.php?action=fileCopy&fileId=901,902,903,904&target=4502
```
Same form for `fileMove`. Returns `{ status: "OK", note: "" }` on success, or `{ status: "ERROR", note }` if `target` doesn't exist. With the default `placement=top`, files are inserted in the request order, each receiving a fresh `runningOrder` (last-RO + 1000 increments) at the destination.

There is no separate "cut" endpoint — use `fileMove` for cut-and-paste semantics, and `folderMove` for the equivalent on extensions.

#### `fileDownload`
Downloads a file's audio, always delivered as **MP3** (128 kbps mono, converted on the fly regardless of the original format).

Identify the file with **one** of:

| Param       | Description                                                                                          |
|-------------|------------------------------------------------------------------------------------------------------|
| `audio`     | The file's `ivrName` (from `filesList`). Most precise — use this when you have it.                   |
| `fileName`  | The file's display name (`name` from `filesList`). Deleted (trashed) files are not matched.          |
| `extension` | Optional with `fileName`: extension ID to scope the name lookup (names repeat across extensions).    |

> `fileId` is **not** accepted here — use `audio` or `fileName`.

Delivery mode:

| Param  | Description                                                                                                       |
|--------|--------------------------------------------------------------------------------------------------------------------|
| `play` | Omit → attachment download (`Content-Disposition: attachment`). `play=1` → inline streaming (`Content-Type: audio/mp3`, `Accept-Ranges: bytes`) — suitable for an `<audio>` element or seekable players. |

```
GET ivrFilesApi.php?action=fileDownload&audio=<ivrName>&apiKey=...
GET ivrFilesApi.php?action=fileDownload&fileName=Greeting&extension=4500&play=1&apiKey=...
```

Notes:
- In attachment mode the suggested filename is a random server-generated name (e.g. `6a3179…mp3`), **not** the display name — save it under your own name.
- On failure the response is plain text (`קובץ לא קיים או משתמש לא מחובר`) with HTTP 200, not JSON — check the `Content-Type` (`audio/mp3` = success) before treating the body as audio.

---

### 6.5 Dropdowns (`getOptions` / `getOptions2`)

Resolves any field whose `fields[key].options === "GET"`.

**Request:** `GET ivrFilesApi.php?action=getOptions2&list=fileTags,mailingLists&apiKey=...`

**Response:**
```json
{
  "fileTags":     [ { "value": "5",   "SYSvalue": "5", "label": "greetings" } ],
  "mailingLists": [ { "value": "1000", "label": "1000-General" } ]
}
```

Known keys: `streamId`, `templateId`, `musicOnHold`, `ivrForms`, `mailingLists`, `creditCardId`, `routerId`, `filesList`, `kesherPhone`, `playTemplate`, `fileTags`.

---

### 6.6 System messages

Every extension can override the built-in voice prompts.

#### `systemMessagesList`
`?extensionId=<id>` → `[{ sm, he, type, value }]` — every prompt code with the current custom value (if any).

#### `getCustomMessages`
`?code=<sm>&extensionId=<id>` → the ordered sequence of custom messages stored for that code on that extension.

#### `saveCustomMessagesBeta`
`POST` JSON body:

```json
{
  "code": "sys-5",
  "extensionId": "4500",
  "messages": [
    { "type": "TEXT", "value": "Welcome to our service" },
    { "type": "FILE", "value": "<ivrName>", "conditions": [ ... ] }
  ]
}
```

#### `updateSystemMessage`
Older single-message save (TEXT or FILE). Prefer `saveCustomMessagesBeta`.

#### `deleteSystemMessage`
`?code=<sm>&extensionId=<id>` → removes all custom messages for that code on that extension.

---

### 6.7 Reports (listening & navigation)

Two read-only report endpoints expose the analytics that **accumulate automatically as calls come in** — no setup required. Both are scoped to your account and require `apiKey`. Date filters are optional and inclusive: `from` / `to` as `YYYY-MM-DD`. A brand-new system returns zeros until calls arrive.

#### `fileListenReport`

How a single audio file was actually consumed — how many people played it, how far they listened, where they dropped off, and what they pressed.

**Request:** `GET ivrFilesApi.php?action=fileListenReport&fileId=60383&apiKey=...`

| Param     | Description                                                              |
|-----------|--------------------------------------------------------------------------|
| `fileId`  | The file's ID (from `filesList`). **Required.**                          |
| `from` / `to` | Optional date range (`YYYY-MM-DD`, inclusive).                       |
| `detail`  | `1` to also include the per-listener breakdown (`listeners[]`).          |

**Response:**
```json
{
  "status": "OK",
  "fileId": 60383,
  "fileName": "Greeting",
  "fileLenMs": 32534,
  "buckets": 50,
  "summary": {
    "listeners": 2, "calls": 4, "windows": 4,
    "listenMs": 67336, "wallSec": 68, "avgMs": 33668,
    "completed": 1, "completionPct": 50, "avgCoveragePct": 54
  },
  "coverage": [ { "fromMs": 0, "toMs": 651, "listeners": 4, "plays": 4, "retention": 4 } ],
  "actions": [ { "action": "key1", "n": 3 } ],
  "byDay":   [ { "day": "2026-06-04", "calls": 4 } ],
  "byHour":  [ 0, 0, 0, "…24 values…" ],
  "listeners": [ "…only when detail=1…" ]
}
```

- **`summary.listeners`** — distinct callers (by phone) who heard the file; **`calls`** — distinct calls; **`windows`** — playback events.
- **`completionPct`** — share of listeners who covered **≥90%** of the file.
- **`avgCoveragePct`** — average share of the file each listener covered; **`avgMs`** — average time listened per listener.
- **`coverage[]`** — the file sliced into `buckets` equal segments (default 50), in order. **`retention`** = how many calls reached that segment (the drop-off curve); `plays` / `listeners` = activity inside it; `fromMs` / `toMs` mark the segment's position inside the file.
- **`actions[]`** — caller actions during playback (key presses, skips, …), most frequent first.
- **`byDay` / `byHour`** — when the file was listened to. Hour is account-local (`Asia/Jerusalem`), an array of 24 values (index = hour).
- **`listeners[]`** (only with `detail=1`) — one row per listener: `phone`, `did`, `time`, `last`, `calls`, `listenMs`, `windows`, `coveragePct`, `reachPct`, `complete`, `lastAct`, `actions`.

Errors: `2` missing `fileId`, `5` file not found / not your account.

#### `extensionVisits`

How callers move **through** your extensions — entries per extension, depth funnel, transitions, exit points and popular paths.

**Request:** whole system — `GET ivrFilesApi.php?action=extensionVisits&apiKey=...`
One extension — add `&extensionId=4500` (or `&path=/1/2`).

| Param         | Description                                                       |
|---------------|-------------------------------------------------------------------|
| `extensionId` | Optional — limit the report to one extension. *(or `path`)*       |
| `from` / `to` | Optional date range (`YYYY-MM-DD`, inclusive).                    |

**Response:**
```json
{
  "status": "OK",
  "summary": { "visits": 812, "calls": 300, "callers": 274, "avgDepth": 2.7, "avgDur": 14, "maxDepth": 6 },
  "byExtension": [
    { "extension": "4500", "extName": "Service", "visits": 210, "calls": 198, "callers": 190, "avgDur": 9 }
  ],
  "byDay":  [ { "day": "2026-06-04", "calls": 57 } ],
  "byHour": [ "…24 values…" ],
  "funnel": [
    { "depth": 1, "calls": 300, "pct": 100, "drop": 0 },
    { "depth": 2, "calls": 205, "pct": 68,  "drop": 32 }
  ],
  "transitions": [ { "from": "1", "to": "4500", "fromName": "Main", "toName": "Service", "count": 140 } ],
  "exits":       [ { "extension": "4500", "name": "Service", "count": 88, "pct": 29 } ],
  "paths":       [ { "seq": ["1","4500"], "names": ["Main","Service"], "count": 120 } ],
  "sampleCalls": 300, "capped": false
}
```

- **`summary`** — `visits` (total extension entries), `calls` (distinct calls), `callers` (distinct phones), `avgDepth` (extensions per call), `avgDur` (avg seconds spent in an extension), `maxDepth`.
- **`byExtension[]`** — entries per extension (which extensions get reached, and by how many), most-visited first. This is the core "entry into extensions" list.
- **`funnel[]`** — how many calls reached each navigation depth, with `pct` of all calls and `drop` = % lost since the previous depth.
- **`transitions[]`** — the most common extension→extension hops (with resolved names).
- **`exits[]`** — the extensions where calls most often ended, and their share.
- **`paths[]`** — the most common navigation sequences (first 4 steps), with resolved `names`.
- **`sampleCalls` / `capped`** — funnel / transitions / exits / paths are computed over the most recent calls; `capped: true` means that sample hit its ceiling (aggregate `summary`, `byExtension`, `byDay`, `byHour` are always over the full range).

---

### 6.8 Live monitoring

#### `activeCalls`

Every call **in progress right now**, enriched with where each caller currently is in the tree, the full path they took to get there, and — for a media-player extension — the file they are listening to. This is a real-time snapshot; poll it (e.g. every 4–5 s) to build a live wallboard.

**Request:** `GET ivrFilesApi.php?action=activeCalls&apiKey=...`

No parameters. Returns the whole system's live calls (newest activity last), capped at 2000 concurrent calls.

**Response:**
```json
{
  "status": "Ok",
  "count": 2,
  "now": 1752900000,
  "calls": [
    {
      "callId": "1752899987.4471",
      "phone": "0501234567",
      "did": "0772222603",
      "seconds": 41,
      "extensionId": "7100",
      "extension": "2",
      "extName": "מכירות",
      "extType": "audioPlayer",
      "path": "1/2",
      "file": { "id": 60383, "name": "ברוכים הבאים", "playing": true },
      "diversion": { "number": "548466096", "name": "", "reason": "unavailable", "counter": "2" },
      "status": "in_call",
      "routeTarget": ""
    }
  ]
}
```

| Field         | Meaning                                                                                              |
|---------------|------------------------------------------------------------------------------------------------------|
| `callId`      | Unique id of the live call (matches `extensionVisits` / journey reports).                             |
| `phone`       | Caller number (national `0…` format). Empty if withheld.                                              |
| `did`         | The number that was dialled to reach the system.                                                      |
| `seconds`     | Elapsed seconds since the call started.                                                               |
| `extensionId` | Internal id of the **current** extension (same value used by `extensionId=` in other endpoints).      |
| `extension`   | The dial digit(s) of the current extension.                                                           |
| `extName`     | Name of the current extension.                                                                        |
| `extType`     | Type of the current extension (`menu`, `audioPlayer`, `simpleRouting`, `queueRouting`, `record`, …). |
| `path`        | The current extension's **dial path** from the root, e.g. `"1/2"` — the digits a caller presses to reach it. Same format accepted by the `path=` parameter elsewhere in this API. |
| `file`        | For an `audioPlayer` extension: `{ id, name, playing }` of the audio file. `null` otherwise.          |
| `diversion`   | Set only when the call arrived **forwarded**: `{ number, name, reason, counter }`. `null` otherwise.  |
| `status`      | `in_call` (talking / in an extension), `waiting` (queued, not yet answered), `routed` (connected to a target). |
| `routeTarget` | When `status` is `routed`, the name of the target the call was routed to.                             |

Notes:
- The current extension is taken from the caller's real-time navigation log, so it is accurate even when the same dial digit is reused under different parents.
- **`file.playing`** — `true` means that file is being played **at this moment**; `false` means it is the last file that finished playing (the caller is between files, or has just pressed a key). Use it to distinguish "listening now" from "listened last".
- **`diversion`** — when a subscriber forwards their line to the system, `phone` still holds the **original caller**, so on its own it does not tell you which line the caller actually dialled. `diversion.number` is that forwarding line. `diversion.reason` is the carrier's reason code — `unconditional` (always forwarded), `user-busy`, `no-answer`, `unavailable`, `deflection`. `diversion.counter` is how many times the call was forwarded. The field is absent (`null`) for ordinary calls.
- Read-only: this endpoint never changes call state.

---

### 6.9 Call history

Past calls, reconstructed from the navigation log. `callHistory` returns one summary row per **inbound** call over a time window; `callJourney` returns the full step-by-step trace of a single call; `outboundCalls` lists calls **dialled out by your SIP extensions** (and click-to-call) with their recordings. All are read-only.

#### `callHistory`

One row per call in a time window — who called, when, how long, which extension they entered first and where they ended up.

**Request:** `GET ivrFilesApi.php?action=callHistory&apiKey=...`

| Param | Meaning |
|-------|---------|
| `from`, `to` | Date range `YYYY-MM-DD` (inclusive). Default: the **last 3 days**. The window is capped at **62 days** — a wider range is clamped to the most recent 62 days. |
| `phone` | Filter by caller number (substring match, digits only). Optional. |
| `did` | Filter by the dialled system number (substring match, digits only). Optional. |
| `hourFrom`, `hourTo` | Restrict to an hour-of-day range `0`–`23` (both required to apply). Optional. |
| `limit` | Max rows, default `50`, capped at `100`. |

**Response:**
```json
{
  "status": "Ok",
  "count": 1,
  "capped": false,
  "from": "2026-08-07",
  "to": "2026-08-10",
  "limit": 50,
  "calls": [
    {
      "callId": "1752899987.4471",
      "phone": "0501234567",
      "did": "0772222603",
      "start": 1752899987,
      "end": 1752900041,
      "seconds": 54,
      "steps": 4,
      "entryExt": "7100",
      "entryName": "תפריט ראשי",
      "lastName": "מכירות",
      "recordings": [
        {
          "recId": "a1b2c3d4e5f6a7b8c9d0a1b2c3d4e5f6a7b8c9d0ab",
          "recUrl": "https://audio.ipsales.co.il/a1b2c3d4e5f6a7b8c9d0a1b2c3d4e5f6a7b8c9d0ab.opus",
          "target": "מכירות",
          "answerPhone": "0501112222",
          "seconds": 48
        }
      ]
    }
  ]
}
```

| Field | Meaning |
|-------|---------|
| `callId` | Unique id of the call — pass it to `callJourney` for the full trace. |
| `phone` | Caller number (national `0…`). Empty if withheld. |
| `did` | The number that was dialled to reach the system. |
| `start`, `end` | Unix timestamps of the first entry and the last activity of the call. |
| `seconds` | Total call span in seconds (`end − start`). |
| `steps` | How many extensions the caller passed through. |
| `entryExt` / `entryName` | The **first** extension entered (internal id / its name at the time). |
| `lastName` | Name of the **last** extension the caller reached. |
| `recordings` | Routing-call recordings made during this call (the same recordings shown in **נתבי שיחות ← היסטוריית שיחות**). One entry per **answered** routing/queue leg; empty array when the call had none. `recId` is the recording id, `recUrl` the playable file (`https://audio.ipsales.co.il/<recId>.opus`), `target` the routing destination (name or number), `answerPhone` who answered, `seconds` the answered-leg length. |

Notes:
- `capped: true` means the row cap was hit — narrow the range or add filters to see more.
- History is grouped by `callId`; each call's ordered steps are available via `callJourney`.

#### `callJourney`

The ordered path of a single call — every extension the caller entered, how long they stayed, and (for media-player extensions) which files played and how much was heard.

**Request:** `GET ivrFilesApi.php?action=callJourney&callId=1752899987.4471&apiKey=...`

| Param | Meaning |
|-------|---------|
| `callId` | Required. The call id from `callHistory` (or `activeCalls`). |

**Response:**
```json
{
  "status": "Ok",
  "callId": "1752899987.4471",
  "head": { "phone": "0501234567", "did": "0772222603", "time": 1752899987 },
  "steps": [
    {
      "extension": "7100",
      "extName": "תפריט ראשי",
      "enterTime": 1752899987,
      "duration": 12,
      "audio": [
        { "fileName": "ברוכים הבאים", "fromSec": 0, "toSec": 8, "lenSec": 8, "heardSec": 8, "action": "menu", "key": "1" }
      ]
    }
  ]
}
```

| Field | Meaning |
|-------|---------|
| `head` | Call header: caller `phone`, dialled `did`, and `time` (unix) of the first step. |
| `steps[].extension` | Internal id of the extension entered at this step. |
| `steps[].extName` | Extension name at the time of the visit. |
| `steps[].enterTime` | Unix timestamp the caller entered this extension. |
| `steps[].duration` | Seconds spent in this extension (`0` = a pass-through / gateway node). |
| `steps[].audio` | For media-player steps: files played. Each: `fileName`, `fromSec`/`toSec` (played offset window), `lenSec` (file length), `heardSec` (seconds actually heard), `action`, and `key` (digit pressed, if any). Empty array otherwise. |

Notes:
- Steps are in chronological order; audio events are attached to the step whose entry time precedes them.
- Read-only.

#### `outboundCalls`

Calls that **left** the system: dialled by one of your SIP extensions from its phone/softphone, or triggered through `click2call`. One row per call, newest first, with the recording of every answered call.

`callHistory` does **not** include these — it is built from the IVR navigation log, and an outbound call never enters the extension tree. Use `outboundCalls` for them.

**Request:** `GET ivrFilesApi.php?action=outboundCalls&apiKey=...`

| Param | Meaning |
|-------|---------|
| `from`, `to` | Date range `YYYY-MM-DD` (inclusive). Default: the **last 3 days**. The window is capped at **62 days** — a wider range is clamped to the most recent 62 days. |
| `extension` | Only calls dialled by this SIP extension number (exact match, e.g. `250`). Optional. |
| `phone` | Filter by the dialled number (substring match, digits only). Optional. |
| `status` | `answered` — only calls that were picked up; `unanswered` — only calls that were not. Optional; default is both. |
| `limit` | Max rows, default `50`, capped at `100`. |

**Response:**
```json
{
  "status": "Ok",
  "count": 2,
  "capped": false,
  "from": "2026-09-19",
  "to": "2026-09-22",
  "limit": 50,
  "calls": [
    {
      "callId": "1758531307.10231",
      "time": 1790071307,
      "extension": "250",
      "target": "0501234567",
      "targetName": "",
      "status": "ANSWER",
      "answered": true,
      "answerPhone": "0501234567",
      "ringSeconds": 2,
      "talkSeconds": 35,
      "recordings": [
        {
          "recId": "b7c2e1f0a9d8c7b6a5f4e3d2c1b0a9f8e7d6c5b4a3",
          "recUrl": "https://audio.ipsales.co.il/b7c2e1f0a9d8c7b6a5f4e3d2c1b0a9f8e7d6c5b4a3.opus"
        }
      ]
    },
    {
      "callId": "1758531265.10229",
      "time": 1790071265,
      "extension": "250",
      "target": "0529876543",
      "targetName": "",
      "status": "CANCEL",
      "answered": false,
      "answerPhone": "",
      "ringSeconds": 6,
      "talkSeconds": 0,
      "recordings": []
    }
  ]
}
```

| Field | Meaning |
|-------|---------|
| `callId` | Unique id of the call. |
| `time` | Unix timestamp when the call was placed. |
| `extension` | The SIP extension number that dialled (as shown in **שלוחות SIP**). |
| `target` | The number that was dialled (national `0…`). |
| `targetName` | Name of the dialled destination when the system knows it (usually empty for free dialling). |
| `status` | Asterisk dial status: `ANSWER`, `NOANSWER`, `BUSY`, `CANCEL` (the extension hung up before answer), `CONGESTION`, `CHANUNAVAIL`. |
| `answered` | `true` when `status` is `ANSWER`. |
| `answerPhone` | The number that answered; empty when unanswered. |
| `ringSeconds` | Seconds from dial until answer or hang-up. |
| `talkSeconds` | Seconds the call was connected (`0` when unanswered). |
| `recordings` | The call recording — one entry for an **answered** call, empty array otherwise. `recId` is the recording id, `recUrl` the playable file (`https://audio.ipsales.co.il/<recId>.opus`). Same recordings as **נתבי שיחות ← היסטוריית שיחות** in the management UI. |

Notes:
- Recording is always on for outbound SIP calls; there is no per-extension switch.
- `capped: true` means the row cap was hit — narrow the range or add `extension` / `status` filters.
- Read-only.

### 6.10 Call events (outbound webhook)

Unlike every other endpoint here (which **you** call), this is a push: the PBX calls **your** URL. When a call enters a **routing** extension (simple / IP / queue routing, or routing triggered from an API module), the PBX sends an HTTP `POST` to your webhook at each lifecycle moment — **waiting**, **answer**, **hangup**.

**Where the URL comes from** (first non-empty wins):
1. `eventsUrl` on the specific routing extension (per-extension override).
2. `eventsUrl` returned in an API-module response that starts routing (per-call override).
3. The system-wide default (Settings → *Call events*).

Empty everywhere = webhooks are off. Configure the default and per-extension URLs in the management UI; there is no apiKey action to set them.

**Delivery:** one `POST` per event, `Content-Type: application/json`, header `X-IPPBX-Event: <event>`. Sent **fire-and-forget** (it never blocks or delays the call), 15-second timeout, no retry. Respond `2xx`; the body is ignored.

**Common fields (every event):**

| Field | Meaning |
|-------|---------|
| `event` | `waiting`, `answer`, or `hangup`. |
| `time` | Unix timestamp when the event fired. |
| `callId` | The call id — matches `activeCalls` / `callHistory` / `callJourney`. |
| `num` | Your system id. |
| `did` | The number that was dialled to reach the system. |
| `phone` | The caller number. |
| `extensionId` | Id of the routing extension. |
| `extensionPath` | Dial path of that extension (e.g. `"1/2"`). |
| `routingType` | `simple`, `ip`, or `queue`. |

**Per-event extras:**

| Event | Extra fields |
|-------|--------------|
| `waiting` | `target` (target/queue name), `targetPhone` (number/queue being dialled). |
| `answer` | `answerPhone` (the number/agent that answered). |
| `hangup` | `status` (`ANSWER`, `NOANSWER`, `BUSY`, `CANCEL`, …), `statusText` (Hebrew label), `answerLength` (seconds the call was connected), `ringLength` (seconds it rang). |

**Example — `answer`:**
```json
POST https://your-server.example/webhook
X-IPPBX-Event: answer
Content-Type: application/json

{
  "event": "answer",
  "time": 1752900041,
  "callId": "1752899987.4471",
  "num": "0772222603",
  "did": "0772222603",
  "phone": "0501234567",
  "extensionId": "7100",
  "extensionPath": "1/2",
  "routingType": "queue",
  "answerPhone": "0509876543"
}
```

Notes:
- Events fire only for **routing** extensions — there is no per-file / menu event.
- `waiting` fires once when the caller enters the router/queue; `answer` fires when a target picks up; `hangup` fires when the routing leg ends (answered **or** not — check `status`).
- Because delivery is fire-and-forget with no retry, treat the webhook as best-effort telemetry; for authoritative records reconcile against `callHistory`.

---

## 7. End-to-end example: create a "press 1 to talk to sales" menu

```
1. GET  getUiSchema                                           → cache schema
2. GET  foldersList                                           → find root ID (e.g. 1)
3. POST extensionSet
        extensionId=NEW, belowExtension=1,
        type=menu, name="Support", extension="9",
        settings[maxDigits]=1, settings[menuReplay]=3
        → returns new ID (e.g. 7100) in foldersList
4. POST extensionSet
        extensionId=NEW, belowExtension=7100,
        type=simpleRouting, extension="1",
        settings[dialPhone]=0501112222, settings[ringSec]=25
5. (Optional) POST securitySet on 7100 to restrict access.
```

---

## 8. Glossary

| Term              | Meaning                                                                                    |
|-------------------|--------------------------------------------------------------------------------------------|
| Extension         | A node in the phone tree; every caller action happens inside one.                          |
| Type              | What an extension does (`menu`, `simpleRouting`, `audioPlayer`, `record`, ...).            |
| Settings          | Type-specific configuration of an extension, stored as JSON.                               |
| Security          | Per-extension access rules stored as ordered `S1, S2, ...` blocks.                         |
| File              | An audio recording attached to an extension.                                               |
| Tag               | A label on a file, enabling playback by tag.                                               |
| Running order     | File playback order inside an extension; editable via `fileOrderSet`.                      |
| Running order view| 1-based human-friendly index derived from `runningOrder`.                                  |
| Schema            | `types` + `fields` + `forms` — the machine-readable description of the UI.                 |
| View-if           | Conditional visibility DSL for form fields (`field:val*val & other:val`).                  |
| Mailing list      | A phone-number list used by several types (mailing/ring-hangup).                           |
| System message    | Built-in voice prompt; can be overridden per-extension with text or audio.                 |

---

## 9. Cheat sheet — every action

| Group            | Action                     | Purpose                                   |
|------------------|----------------------------|-------------------------------------------|
| Schema           | `getUiSchema`              | Everything at once.                       |
| Schema           | `getExtensionTypes`        | Type list only.                           |
| Schema           | `getFieldsSchema`          | Field definitions only.                   |
| Schema           | `getFormLayout`            | Form layout for a type.                   |
| Schema           | `getAccountSettings`       | Account flags (`use_rh`, `rh_max`, ...).  |
| Schema           | `getMailingLists`          | Clean mailing-list JSON.                  |
| Schema           | `getExtensionPath`         | Breadcrumb for one extension.             |
| Tree             | `foldersList`              | Full tree.                                |
| Tree             | `folderCopy`               | Recursive copy.                           |
| Tree             | `folderMove`               | Reparent.                                 |
| Tree             | `folderDelete`             | Delete (soft-deletes files).              |
| Extension        | `folderSettings`           | Load one extension.                       |
| Extension        | `extensionSet`             | Create / update.                          |
| Extension        | `securitySet`              | Save access rules.                        |
| Files            | `filesList`                | Files of an extension.                    |
| Files            | `uploadFile`               | Upload audio (multipart, ≤60 MB).         |
| Files            | `uploadInit`               | Open a chunked-upload session.            |
| Files            | `uploadChunk`              | Send one part (multipart).                |
| Files            | `uploadComplete`           | Assemble + validate + store.              |
| Files            | `uploadStatus`             | Which parts arrived (for resume).         |
| Files            | `uploadAbort`              | Discard a chunked session.                |
| Files            | `fileDelete`               | Soft delete.                              |
| Files            | `fileRename`               | Rename.                                   |
| Files            | `fileTagging`              | Set tags.                                 |
| Files            | `fileOrderSet`             | Reorder.                                  |
| Files            | `fileCopy` / `fileMove`    | Copy / move to another extension.         |
| Files            | `fileDownload`             | Stream the audio.                         |
| Dropdowns        | `getOptions` / `getOptions2`| Resolve `"options": "GET"` fields.       |
| System messages  | `systemMessagesList`       | Codes + current custom values.            |
| System messages  | `getCustomMessages`        | Stored sequence for one code.             |
| System messages  | `saveCustomMessagesBeta`   | Save sequence (JSON body).                |
| System messages  | `updateSystemMessage`      | Legacy single save.                       |
| System messages  | `deleteSystemMessage`      | Remove all custom values for a code.      |
| Reports          | `fileListenReport`         | Per-file listening report (drop-off, completion, actions). |
| Reports          | `extensionVisits`          | Extension-entry / navigation report (funnel, transitions, exits, paths). |
| Live             | `activeCalls`              | Calls in progress now — current extension, path, file being played, and forwarding party. |
| History          | `callHistory`              | Past calls — one summary row per call over a time window (caller, duration, entry/last extension). |
| History          | `callJourney`              | Full step-by-step trace of one call (extensions entered, time spent, files heard). |
