# 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://app.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`.

---

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

#### `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.                       |
| `target`      | Destination parent ID.                                           |

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&project=main
```
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.           |
| `target`      | Destination parent ID.                  |

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

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

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

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

**Bulk example** — copy four files to extension `4502`:
```
GET ivrFilesApi.php?action=fileCopy&fileId=901,902,903,904&target=4502&project=main
```
Same form for `fileMove`. Returns `{ status: "OK", note: "" }` on success, or `{ status: "ERROR", note }` if `target` doesn't exist. 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`
Streams the audio (proxied via `audio.php`). Typical UIs link directly to `audio.php?type=ivr&audio=<ivrName>` instead.

---

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

---

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