openapi: 3.1.0

info:
  title: AutoCall.kz API
  version: "1.0"
  description: |
    REST API сервиса автообзвона AutoCall.kz.

    ## Публичный API
    Эндпоинты `/api/public/*` доступны без аутентификации — тарифы и тестовый звонок.

    ## API v1 (авторизованный)
    Все эндпоинты `/api/v1/*` требуют заголовок:
    ```
    Authorization: Bearer ВАШ_API_КЛЮЧ
    ```
    API ключ доступен в разделе **Настройки → API** на портале autocall.kz.

    ## Лимиты
    Ограничений два, и они независимы — сработает то, до которого дойдёте раньше.

    | Уровень | Лимит | На что считается |
    |---------|-------|------------------|
    | Приложение | **300 запросов в минуту** | на ваш API-ключ |
    | Сервер | **15 запросов в секунду**, всплеск до 30 | на IP-адрес |

    Оба отвечают статусом `429`. Лимит приложения возвращает JSON с полем `message`;
    серверный отдаёт короткую HTML-страницу — на него не стоит рассчитывать как на
    предсказуемый ответ, это защита от флуда.

    Отдельно ограничено число **одновременных** соединений с одного адреса — 20.

    ### Как не упираться

    - Держите не больше 10–15 запросов в секунду и делайте паузу при `429`,
      желательно с экспоненциальной выдержкой.
    - Не открывайте десятки параллельных соединений: последовательные запросы в
      2–3 потока быстрее, чем 20 потоков, половина которых получает `429`.
    - Систематическое превышение серверного лимита может привести к временной
      блокировке адреса. Если ваша интеграция требует большего — напишите в
      поддержку, лимит поднимут точечно.

    Для увеличения лимита приложения обратитесь в поддержку.

    ## Вебхуки (`callback_url`)

    Адрес приёмника задаётся двумя способами:

    - **Глобально** — в личном кабинете, **Настройки → API**, поле «Адрес для вебхуков».
      Применяется ко всем обзвонам и рассылкам аккаунта, включая созданные вручную в кабинете.
      Принимается только `https`.
    - **Для конкретного обзвона или рассылки** — поле `callback_url` в теле запроса на
      создание. Оно **перебивает** глобальный адрес: события по этому объекту уйдут только
      на него.

    Если не задано ни то, ни другое — вебхуки не отправляются.

    Мы шлём `POST` с `Content-Type: application/json` по мере изменения объектов:
    `{"call": {...}}` — про звонок, `{"autocall": {...}}` — про обзвон,
    `{"message": {...}}` / `{"bulk": {...}}` — про SMS.

    Что важно учесть при интеграции:

    - **События звонка схлопываются.** Звонок меняется по ходу жизни несколько раз
      (постановка в набор, результат попытки, финализация, распознавание речи,
      расшифровка разговора агента), но отдельный запрос на каждое изменение мы НЕ шлём.
      После каждого изменения запускается пауза в 30 секунд; новое изменение сбрасывает
      её заново. Как только звонок 30 секунд не менялся, уходит один `POST` с его
      состоянием на этот момент.

      Поэтому изменения, идущие подряд, склеиваются в один запрос, а разнесённые во
      времени — нет. Разговор длиннее 30 секунд даст два запроса: сначала о том, что
      звонок начался (`status: calling`), потом о результате. Это ожидаемое поведение,
      а не сбой.
    - **Следствие: данные приходят с задержкой** примерно в полминуты после последнего
      изменения — и уже полными. В частности, у агент-звонков расшифровка разговора
      (`data._transcript`) на этот момент записана и обработана; раньше она могла не
      успеть.
    - **Один объект всё равно может прийти несколько раз.** Схлопываются изменения
      ВНУТРИ одной паузы; между попытками дозвона проходят минуты, поэтому на каждую
      попытку придёт свой запрос. Обрабатывайте вебхуки идемпотентно — по `call.id` /
      `autocall.id`, а не «по факту получения».
    - **События SMS-сообщений схлопываются так же** и по тому же правилу: склеивается
      то, что уложилось в одну паузу. Статус доставки от оператора приходит минутами позже
      отправки, то есть заведомо за пределами паузы: это отдельный запрос, и так и
      задумано — новая информация, а не уточнение прежней.
    - **События обзвона и рассылки не схлопываются.** Переходы `moderation → awaiting →
      running → completed` приходят отдельными запросами: это разные по смыслу
      состояния, разнесённые во времени.
    - **Порядок доставки не гарантирован.** События звонков и события обзвона идут
      независимо, поэтому `autocall.status = completed` может прийти раньше последнего
      события звонка. Ориентируйтесь на содержимое, а не на очерёдность.
    - **Объём.** Обзвон на N номеров с одной попыткой даёт от `4 + N` до `4 + 2 × N`
      запросов: четыре по обзвону и один-два на номер — в зависимости от того, длиннее
      разговор паузы схлопывания или короче. Против прежних `4 + 4 × N` это вдвое-вчетверо
      меньше.
    - **Повторов нет.** Ответ мы не проверяем и при ошибке не повторяем, но соединение
      держим до 10 секунд. Медленный приёмник вредит прежде всего вам: часть событий
      просто не доедет. Отвечайте `200` сразу и обрабатывайте асинхронно.
    - **Редиректы не поддерживаются.** `301`/`302` не выполняются — указывайте сразу
      конечный URL (в частности, `https://`, а не `http://` с редиректом).
    - **Только публичные IPv4-адреса.** Приватные диапазоны и IPv6-only хосты
      недоступны.

    ## Форматы дат
    - Дата: `Y-m-d` (например: `2024-03-15`)
    - Время: `H:i:s` (например: `13:00:00`)
    - Дата + время: `Y-m-d H:i:s`
  contact:
    name: Поддержка AutoCall.kz
    email: info@autocall.kz
    url: https://autocall.kz

servers:
  - url: https://autocall.kz/api/v1
    description: API v1 (требует Bearer токен)

security:
  - bearerAuth: []

tags:
  - name: Публичный API
    description: Эндпоинты без аутентификации — тарифы и тестовый звонок.
  - name: Списки контактов
    description: Создание и управление списками контактов для обзвонов и рассылок.
  - name: Контакты
    description: Просмотр и удаление отдельных контактов внутри списков.
  - name: Аудиозаписи
    description: Загрузка аудиофайлов и синтез речи для использования в обзвонах.
  - name: Сценарии
    description: |
      Интерактивные сценарии для обзвонов с распознаванием речи, нажатием клавиш и ветвлением логики.

      ## Поле `data` — это JSON, сериализованный в СТРОКУ

      Сценарий передаётся не объектом, а **строкой** с закодированным JSON:
      `json_encode()` в PHP, `JSON.stringify()` в JS, `json.dumps()` в Python.
      Если прислать объект — придёт `422` с текстом
      «Значение поля data должно быть закодированной json строкой».

      ```php
      $scenario = [ /* граф, см. ниже */ ];
      $body = ['name' => 'Опрос', 'data' => json_encode($scenario, JSON_UNESCAPED_UNICODE)];
      ```

      В ответах (`GET /scenarios/{id}`) поле `data` приходит уже **объектом**, а не строкой —
      перед отправкой обратно в `PUT` его нужно снова сериализовать.

      ## Структура графа

      `data` — объект, где **ключ = id элемента**, значение = сам элемент. Массив не подойдёт.
      Переходы между элементами — по id.

      Обязательный скелет любого сценария:

      - ровно один элемент `start` — точка входа, его `next` ведёт в «ждём речи»;
      - ровно один `talk_detect` — «ждём, пока абонент возьмёт трубку и скажет „алло“».
        Он обязан лежать под ключом **`TALK_DETECT`**: этот id зашит в генераторе дайлплана;
      - минимум один `end` с `next: null` — в него в итоге должны приходить все ветки,
        иначе часть сценария окажется недостижимой.

      Id остальных элементов придумываете вы: **латиница, цифры, `_`, `.` и `-`, до 64 символов**
      (id становится именем экстеншена в дайлплане, поэтому набор символов ограничен). Удобнее
      всего uuid (`a1b2c3d4`), но подойдут и осмысленные — `greeting`, `menu`. Для `start` и `end`
      принято брать `START` и `END`. Поле `id` внутри элемента обязано совпадать с его ключом.

      ## Общие поля элемента

      | Поле   | Тип             | Описание |
      |--------|-----------------|----------|
      | `id`   | string          | Совпадает с ключом в объекте `data`. |
      | `type` | string          | Тип элемента (таблица ниже). |
      | `name` | string          | Название — видно в отчётах и на схеме сценария. У системных `start`/`talk_detect`/`end` можно не задавать. |
      | `next` | string \| null  | id следующего элемента. Только у линейных типов; у ветвящихся переходы лежат внутри `data`. |

      Граф собирается по переходам (`next` и ветки внутри `data`), обратные рёбра считать не нужно.
      Каждый элемент должен быть достижим из `start` — иначе 422. Поле `prev`, которое требовали
      старые версии API, теперь необязательно: если вы его присылаете, оно просто игнорируется.

      ## Типы элементов и их `data`

      **`playback` — проиграть аудиозапись.** `data` = id аудиозаписи (число/строка, ваша запись
      из `GET /sounds`), плюс `next`.
      ```json
      { "id": "greet", "type": "playback", "name": "Приветствие", "next": "menu", "data": 427760 }
      ```

      **`record` — записать речь абонента.** `data.duration` — сколько писать, 5–60 с;
      `data.silence` — пауза, после которой запись обрывается, 2–5 с. Плюс `next`.
      Запись потом слушается в детализации обзвона.
      ```json
      { "id": "rec", "type": "record", "name": "Ответ", "next": "END",
        "data": { "duration": 20, "silence": 3 } }
      ```

      **`dtmf` — проиграть аудио и ждать нажатия клавиши.** `data.id` — id аудиозаписи с вопросом;
      `data.directions[]` — ветки, где `keywords` = ровно одна цифра `0`–`9`;
      `data.timeout` — `{duration, next}`, куда идти, если абонент молчит (`duration` 5–20 с);
      `data.else.next` — куда идти при нажатии, которого нет в ветках;
      `data.reask.enabled` — переспрос (`false`, если не нужен).

      **`speech` — проиграть аудио и распознать РЕЧЬ.** Поля те же, что у `dtmf`, но
      `keywords` — список ожидаемых слов через запятую (буквы, пробелы, запятые), а `timeout.duration`
      берите 1–10 с. Дополнительно `data.question` — сам вопрос текстом: если ответ абонента не совпал
      ни с одним списком слов, вопрос и ответ уходят ИИ-классификатору, и он выбирает ветку сам.
      Заполняйте `question` — это заметно поднимает точность.
      ```json
      { "id": "ask", "type": "speech", "name": "Интересно?", "data": {
          "id": 427759,
          "question": "Интересно ли вам предложение?",
          "directions": [
            { "keywords": "да,конечно,интересно,давайте", "next": "sms1" },
            { "keywords": "нет,не надо,не интересно,отказ", "next": "END" }
          ],
          "timeout": { "duration": 7, "next": "END" },
          "else": { "next": "END" },
          "reask": { "enabled": false }
        } }
      ```
      Каждый `speech` тарифицируется отдельно (см. цену распознавания в тарифах).

      **`dial` — соединить абонента с вашим номером.** `data.number` — **id** подтверждённого номера
      (не сам номер, список — в `GET /settings/confirmed_numbers`); `data.duration` — лимит
      разговора 60–600 с;
      `data.timeout.next` — ⚠️ куда идти, если **дозвонились** (историческое название поля, не таймаут);
      `data.else.next` — если дозвониться не удалось.

      **`sms` — SMS абоненту.** `data.message` — текст, плюс `next`. Поддерживаются
      `{{переменные}}` контакта. Длина не ограничена одним сообщением: длинный текст уйдёт
      несколькими частями и так же посчитается в стоимости (70 символов кириллицей на часть,
      160 латиницей). Номер указывать не нужно — SMS всегда уходит абоненту, которому звоним.

      **`sms_me` — SMS себе.** То же самое, но `data.number` — id вашего подтверждённого номера,
      и он используется по-настоящему. К тексту автоматически добавляется номер абонента.

      **`request` — HTTP-запрос на ваш сервер.** `data.url` (обязателен, `http://` или `https://`),
      `data.method` (по умолчанию `POST`), `data.headers` — массив `{key, value}`,
      `data.bodyType` — `none` \| `json` \| `form`, `data.body` — тело для `json`,
      `data.params` — массив `{key, value}` для `form`, `data.save_var` — имя переменной
      (латиница, цифры, `_`), куда положить ответ, чтобы использовать его дальше как `{{имя}}`.
      В URL, заголовках и теле работают `{{переменные}}`.
      ```json
      { "id": "hook", "type": "request", "name": "Отдать лид в CRM", "next": "END",
        "data": { "url": "https://example.kz/api/lead", "method": "POST", "headers": [],
                  "bodyType": "json", "body": "{\"phone\":\"{{phone}}\",\"name\":\"{{name}}\"}" } }
      ```
      Запросы на приватные адреса (`10.0.0.0/8`, `127.0.0.1`, метаданные облаков) блокируются.

      ## Элементы с переменными

      Эти пять типов появились вместе с новым конструктором и работают через API наравне
      с остальными. Имена переменных — латиница, цифры и `_`; в текстах, URL и телах запросов
      они подставляются как `{{имя}}` вперемешку с переменными контакта.

      **`say` — озвучить текст (TTS).** `data.text` — что сказать, плюс `next`. Удобно, когда
      фраза зависит от данных: `«Здравствуйте, {{name}}, ваш заказ {{order}} готов»`.
      Тарифицируется по цене синтеза.

      **`stt_var` — записать ответ абонента в переменную.** `data.duration` 5–60 с,
      `data.silence` 2–5 с, `data.variable` — имя переменной, плюс `next`. В отличие от `speech`
      никакого ветвления не делает: просто кладёт распознанный текст в переменную.

      **`setvar` — задать переменную.** `data.variable`, `data.value` (до 255 символов), плюс `next`.

      **`ifelse` — условие.** `data.conditions` — массив `{variable, operator, value}`;
      `data.logic` — `AND` (по умолчанию) или `OR`, если условий несколько;
      `data.then.next` — куда идти, если условие выполнено, `data.else.next` — если нет.

      **`switchvar` — мультиветка по значению переменной.** `data.variable`, `data.operator`,
      `data.cases` — массив `{value, next}` (перебираются сверху вниз, побеждает первый
      подошедший), `data.else.next` — если ни один кейс не подошёл.

      Операторы сравнения: `===`, `!==`, `contains` (подстрока, без учёта регистра),
      `>`, `<`, `>=`, `<=` (числа сравниваются как числа, остальное — как строки).

      ```json
      {
        "ask":   { "id": "ask", "type": "stt_var", "name": "Спросить город", "next": "check",
                   "data": { "duration": 15, "silence": 3, "variable": "city" } },
        "check": { "id": "check", "type": "ifelse", "name": "Это Астана?",
                   "data": { "conditions": [ { "variable": "city", "operator": "contains", "value": "астана" } ],
                             "logic": "AND", "then": { "next": "greet" }, "else": { "next": "END" } } }
      }
      ```

      ## Пример 1 — минимальный: поздоровались и положили трубку

      ```json
      {
        "START":       { "id": "START", "type": "start", "name": "Начало", "next": "TALK_DETECT" },
        "TALK_DETECT": { "id": "TALK_DETECT", "type": "talk_detect", "name": "Ждём речи", "next": "greet" },
        "greet":       { "id": "greet", "type": "playback", "name": "Приветствие", "next": "END", "data": 427760 },
        "END":         { "id": "END", "type": "end", "name": "Конец", "next": null }
      }
      ```

      ## Пример 2 — распознавание речи: согласным шлём SMS

      ```json
      {
        "START":       { "id": "START", "type": "start", "name": "Начало", "next": "TALK_DETECT" },
        "TALK_DETECT": { "id": "TALK_DETECT", "type": "talk_detect", "name": "Ждём речи", "next": "greet" },
        "greet":       { "id": "greet", "type": "playback", "name": "Приветствие", "next": "ask", "data": 427760 },
        "ask": {
          "id": "ask", "type": "speech", "name": "Интересно предложение?", "data": {
            "id": 427759,
            "question": "Интересно ли вам наше предложение?",
            "directions": [
              { "keywords": "да,конечно,интересно,давайте,расскажите", "next": "sms1" },
              { "keywords": "нет,не надо,не интересно,некогда", "next": "END" }
            ],
            "timeout": { "duration": 7, "next": "END" },
            "else":    { "next": "END" },
            "reask":   { "enabled": false }
          }
        },
        "sms1": {
          "id": "sms1", "type": "sms", "name": "Подробности в SMS", "next": "END",
          "data": { "message": "Спасибо! Подробности: example.kz" }
        },
        "END": { "id": "END", "type": "end", "name": "Конец", "next": null }
      }
      ```
      В `END` тут приходят сразу четыре перехода: ветка «нет», таймаут и «иначе» у `ask` плюс
      `sms1`. Перечислять их где-то отдельно не нужно — граф строится по самим переходам.

      ## Пример 3 — DTMF-меню с переводом на менеджера

      ```json
      {
        "START":       { "id": "START", "type": "start", "name": "Начало", "next": "TALK_DETECT" },
        "TALK_DETECT": { "id": "TALK_DETECT", "type": "talk_detect", "name": "Ждём речи", "next": "menu" },
        "menu": {
          "id": "menu", "type": "dtmf", "name": "Меню", "data": {
            "id": 427759,
            "directions": [
              { "keywords": "1", "next": "call_manager" },
              { "keywords": "2", "next": "notify" }
            ],
            "timeout": { "duration": 10, "next": "END" },
            "else":    { "next": "END" },
            "reask":   { "enabled": false }
          }
        },
        "call_manager": {
          "id": "call_manager", "type": "dial", "name": "Соединить с менеджером", "data": { "number": 4321, "duration": 300, "timeout": { "next": "END" }, "else": { "next": "notify" } }
        },
        "notify": {
          "id": "notify", "type": "sms_me", "name": "Сообщить мне", "next": "END",
          "data": { "message": "Абонент просит перезвонить", "number": 4321 }
        },
        "END": { "id": "END", "type": "end", "name": "Конец", "next": null }
      }
      ```
      `427759`/`427760` — id ваших аудиозаписей (`GET /sounds`), `4321` — id подтверждённого номера
      (`GET /settings/confirmed_numbers`). Подставьте свои: чужие id вернут 422.

      ## Частые ошибки 422

      | Сообщение | Причина |
      |-----------|---------|
      | Значение поля data должно быть закодированной json строкой | Прислали объект вместо строки либо список вместо объекта «id → элемент». |
      | Элемент «…» имеет неверную структуру (нет type) | У элемента нет поля `type` либо вместо объекта лежит строка/число. |
      | Недопустимый тип элемента у «…» | Опечатка в `type`. |
      | Недопустимый id элемента «…» | В id есть что-то кроме латиницы, цифр, `_`, `.`, `-` либо он длиннее 64 символов. |
      | У элемента «…» поле id не совпадает с его ключом | `"a1": { "id": "a2", … }` — так нельзя. |
      | В сценарии должен быть ровно один элемент «Начало звонка» / «Ждём речи» | Нет `start`/`talk_detect` или их несколько. |
      | Элемент «Ждём речи» должен лежать под ключом TALK_DETECT | `talk_detect` положили под своим id. |
      | После «Начало звонка» должно идти «Ждём речи» | `start.next` ведёт не в `talk_detect`. |
      | В сценарии должен быть хотя бы один элемент «Положить трубку» | Нет ни одного `end`. |
      | У элемента «…» есть переход на несуществующий элемент | `next` (или `next` внутри ветки) ссылается на id, которого нет в объекте. |
      | Элемент «…» недостижим (в него не ведёт ни одна стрелка) | В элемент нет входа из `start` — забыли его подключить. |
      | Элемент «…» должен куда-то вести | Пустой `next` у линейного элемента. |
      | У «…» не указана аудиозапись / Аудиозапись у «…» вам не принадлежит | Нет `data.id` (или `data` у `playback`) либо id чужой записи. |
      | У «…» добавьте хотя бы одну ветку / ветка никуда не ведёт | Пустой `directions[]` или ветка без `next`. |
      | У «…» не задан переход по таймауту / «иначе» | Нет `data.timeout.next` или `data.else.next`. |
      | У «…» таймаут должен быть …–… секунд | `timeout.duration` вне диапазона: `dtmf` 5–20, `speech` 1–10. |
      | У «…» некорректный URL | В `request` не `http(s)://…`. |
      | У «…» имя переменной: только латиница, цифры и _ | Кириллица или пробел в `variable`/`save_var`. |

      Стоимость сценария (`max_cost`) считается сервером сам — как самая дорогая цепочка по графу,
      и возвращается в ответе.
  - name: ИИ-агенты
    description: |
      Голосовые ИИ-агенты: живой разговор с абонентом вместо жёсткого сценария.

      Агент сам ведёт диалог, отвечает на вопросы, извлекает из разговора нужные вам поля
      (`data_schema`) и может по ходу разговора отправить SMS или сходить HTTP-запросом
      на ваш сервер (`tools`).

      **Жизненный цикл:**
      1. `POST /agents` — агент создаётся со статусом `generating`;
      2. фоновый профилировщик считает его параметры (это секунды, реже — минуты);
      3. статус становится `ready` — только с ним агента можно поставить в обзвон;
      4. `POST /autocalls` с `type=agent` и `agent_id`.

      **Правка агента** (`PUT /agents/{id}`) с новым `prompt` или `max_seconds` снова переводит
      его в `generating`. Уже запущенные обзвоны это не затрагивает — они звонят по слепку,
      снятому в момент создания обзвона.
  - name: Обзвоны
    description: Создание и управление автоматическими обзвонами.
  - name: SMS рассылки
    description: Создание и управление SMS рассылками.
  - name: Настройки
    description: Управление настройками аккаунта, номерами и визитной карточкой.
  - name: Платежи
    description: Управление балансом и просмотр транзакций.
  - name: Панель мониторинга
    description: Сводная статистика и активность аккаунта.
  - name: Уведомления
    description: Получение и управление уведомлениями аккаунта.
  - name: Flash Calls
    description: |
      Верификация пользователей через звонок (Flash Call / Missed Call OTP).

      Сервис совершает звонок на номер абонента. Последние N цифр номера звонящего и являются кодом подтверждения.
      Звонок сбрасывается автоматически — биллинг на стороне абонента не возникает.

      **Два режима:**
      - **Авто-код** (`digits`) — сервис сам генерирует случайный код из N цифр. Работает всегда.
      - **Свой код** (`code`) — вы передаёте нужный код. Может вернуть 422 если нужного номера нет в пуле.

      **Rate limit:** не более 3 попыток на один номер в течение 5 минут.

paths:

  # ─────────────────────────────────────────
  # ПУБЛИЧНЫЙ API (без авторизации)
  # ─────────────────────────────────────────

  /lists:
    get:
      summary: Получить все списки контактов
      operationId: get-lists
      tags: [Списки контактов]
      description: Возвращает все списки контактов с постраничным выводом. Поддерживает фильтрацию по дате и client_id.
      parameters:
        - $ref: '#/components/parameters/ContentType'
        - $ref: '#/components/parameters/Accept'
        - $ref: '#/components/parameters/DateFrom'
        - $ref: '#/components/parameters/DateTo'
        - $ref: '#/components/parameters/Order'
        - $ref: '#/components/parameters/ClientIdFilter'
        - $ref: '#/components/parameters/Page'
      responses:
        '200':
          description: Объект постраничного вывода со списками контактов.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Pagination'
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: '#/components/schemas/ContactList'
              example:
                current_page: 1
                data:
                  - id: 73
                    client_id: "123e4567-e89b-12d3-a456-426655440000"
                    name: "Клиенты март"
                    importing: false
                    nonexistent: null
                    created_at: "2024-03-31 05:55:19"
                    updated_at: "2024-03-31 05:55:19"
                    contacts_count: 250
                    used: 3
                first_page_url: "https://autocall.kz/api/v1/lists?page=1"
                from: 1
                last_page: 2
                last_page_url: "https://autocall.kz/api/v1/lists?page=2"
                next_page_url: "https://autocall.kz/api/v1/lists?page=2"
                path: "https://autocall.kz/api/v1/lists"
                per_page: 23
                prev_page_url: null
                to: 23
                total: 27
        '401': { $ref: '#/components/responses/401' }
        '404':
          description: Список не найден по указанному `client_id`.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorMessage' }
        '422': { $ref: '#/components/responses/422' }
        '429': { $ref: '#/components/responses/429' }

    post:
      summary: Создать новый список контактов
      operationId: post-lists
      tags: [Списки контактов]
      description: |
        Создаёт новый список контактов. Максимум 10 000 номеров в одном запросе.
        Для загрузки большего количества — создайте список с первыми 10 000 контактами, затем дополните через `/lists/append/{id}`.
      parameters:
        - $ref: '#/components/parameters/ContentType'
        - $ref: '#/components/parameters/Accept'
      requestBody:
        required: true
        description: Параметры нового списка контактов.
        content:
          application/json:
            schema:
              type: object
              required: [name, numbers]
              properties:
                name:
                  type: string
                  minLength: 1
                  maxLength: 40
                  description: Название списка
                numbers:
                  type: array
                  maxItems: 10000
                  description: Массив с номерами телефонов
                  items:
                    type: object
                    required: [number]
                    properties:
                      number:
                        type: string
                        minLength: 12
                        maxLength: 12
                        pattern: '^\+7(700|701|702|705|707|708|747|771|775|776|777|778|710|711|712|713|714|715|716|717|718|721|722|723|724|725|726|727|728|729|736)\d{7}$'
                        description: Номер телефона в формате +7XXXXXXXXXX
                      extra:
                        type: string
                        maxLength: 255
                        description: Дополнительное описание (ФИО, ИИН и т.п.)
                      variables:
                        type: object
                        description: |
                          Переменные для персонализации SMS. Используются в тексте рассылки через `{{varname}}`.
                          Ключи — только `[a-zA-Z0-9_]`, до 30 символов. Значения — строки до 255 символов.
                        additionalProperties: { type: string }
                        example: { name: "Иванов Иван", debt: "15000" }
                client_id:
                  type: string
                  maxLength: 256
                  description: ID в Вашей системе
            examples:
              Простой список:
                value:
                  name: "Клиенты апрель"
                  numbers:
                    - number: "+77010000001"
                    - number: "+77010000002"
              С описанием и client_id:
                value:
                  name: "VIP клиенты"
                  client_id: "vip-2024"
                  numbers:
                    - number: "+77010000001"
                      extra: "Иванов И.И."
                    - number: "+77010000002"
                      extra: "Петров П.П."
              С переменными:
                value:
                  name: "Должники май"
                  numbers:
                    - number: "+77010000001"
                      extra: "Иванов Иван"
                      variables:
                        name: "Иван"
                        debt: "15000"
                        contract: "№А-123"
                    - number: "+77010000002"
                      extra: "Петров Пётр"
                      variables:
                        name: "Пётр"
                        debt: "3200"
                        contract: "№Б-456"
      responses:
        '201':
          description: Список успешно создан.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContactList'
              example:
                id: 74
                name: "Клиенты апрель"
                client_id: "vip-2024"
                contacts_count: 2
                created_at: "2024-04-11 22:14:09"
                updated_at: "2024-04-11 22:14:09"
        '401': { $ref: '#/components/responses/401' }
        '422': { $ref: '#/components/responses/422' }
        '429': { $ref: '#/components/responses/429' }
        '500': { $ref: '#/components/responses/500' }

  /lists/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema: { type: integer }
        description: ID списка
    get:
      summary: Показать список
      operationId: get-lists-id
      tags: [Списки контактов]
      description: Возвращает детальную информацию о списке с контактами (постраничный вывод).
      parameters:
        - $ref: '#/components/parameters/ContentType'
        - $ref: '#/components/parameters/Accept'
        - $ref: '#/components/parameters/Page'
      responses:
        '200':
          description: Объект списка с контактами.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ContactList'
                  - type: object
                    properties:
                      contacts:
                        allOf:
                          - $ref: '#/components/schemas/Pagination'
                          - type: object
                            properties:
                              data:
                                type: array
                                items:
                                  $ref: '#/components/schemas/Contact'
        '401': { $ref: '#/components/responses/401' }
        '404': { $ref: '#/components/responses/404' }
        '429': { $ref: '#/components/responses/429' }
    put:
      summary: Редактировать список
      operationId: put-lists-id
      tags: [Списки контактов]
      description: Изменяет название или client_id списка контактов.
      parameters:
        - $ref: '#/components/parameters/ContentType'
        - $ref: '#/components/parameters/Accept'
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                  minLength: 1
                  maxLength: 40
                client_id:
                  type: string
                  maxLength: 256
      responses:
        '200':
          description: Обновлённый объект списка.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ContactList' }
        '401': { $ref: '#/components/responses/401' }
        '404': { $ref: '#/components/responses/404' }
        '422': { $ref: '#/components/responses/422' }
        '429': { $ref: '#/components/responses/429' }
        '500': { $ref: '#/components/responses/500' }
    delete:
      summary: Удалить список
      operationId: delete-lists-id
      tags: [Списки контактов]
      description: Удаляет список контактов и все контакты в нём.
      parameters:
        - $ref: '#/components/parameters/ContentType'
        - $ref: '#/components/parameters/Accept'
      responses:
        '204':
          description: Список удалён.
        '401': { $ref: '#/components/responses/401' }
        '404': { $ref: '#/components/responses/404' }
        '422': { $ref: '#/components/responses/422' }
        '429': { $ref: '#/components/responses/429' }
        '500': { $ref: '#/components/responses/500' }

  /lists/append/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema: { type: integer }
        description: ID списка, который нужно дополнить
    post:
      summary: Дополнить список контактов
      operationId: post-lists-append-id
      tags: [Списки контактов]
      description: Добавляет номера в уже существующий список контактов.
      parameters:
        - $ref: '#/components/parameters/ContentType'
        - $ref: '#/components/parameters/Accept'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [numbers]
              properties:
                numbers:
                  type: array
                  maxItems: 10000
                  items:
                    type: object
                    required: [number]
                    properties:
                      number:
                        type: string
                        minLength: 12
                        maxLength: 12
                        pattern: '^\+7(700|701|702|705|707|708|747|771|775|776|777|778|710|711|712|713|714|715|716|717|718|721|722|723|724|725|726|727|728|729|736)\d{7}$'
                      extra:
                        type: string
                        maxLength: 255
                      variables:
                        type: object
                        description: Переменные для персонализации SMS. Ключи — `[a-zA-Z0-9_]`.
                        additionalProperties: { type: string }
      responses:
        '201':
          description: Список успешно дополнен.
          content:
            application/json:
              schema:
                type: object
                properties:
                  id: { type: integer }
                  client_id: { type: [string, "null"] }
                  contacts_count: { type: integer }
                  created_at: { type: string }
                  updated_at: { type: string }
        '401': { $ref: '#/components/responses/401' }
        '404': { $ref: '#/components/responses/404' }
        '422': { $ref: '#/components/responses/422' }
        '429': { $ref: '#/components/responses/429' }

  /lists/nonexistent/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema: { type: integer }
        description: ID списка
    delete:
      summary: Удалить несуществующие номера из списка
      operationId: delete-lists-nonexistent-id
      tags: [Списки контактов]
      description: |
        Удаляет из списка номера, которые оператор пометил как несуществующие в ходе обзвонов.
        Несуществующие номера появляются в поле `nonexistent` после завершения обзвона.
      parameters:
        - $ref: '#/components/parameters/ContentType'
        - $ref: '#/components/parameters/Accept'
      responses:
        '204':
          description: Несуществующие номера удалены.
        '401': { $ref: '#/components/responses/401' }
        '404': { $ref: '#/components/responses/404' }
        '429': { $ref: '#/components/responses/429' }
        '500': { $ref: '#/components/responses/500' }
    put:
      summary: Проигнорировать несуществующие номера
      operationId: put-lists-nonexistent-id
      tags: [Списки контактов]
      description: Сбрасывает флаг несуществующих номеров в списке без их удаления.
      parameters:
        - $ref: '#/components/parameters/ContentType'
        - $ref: '#/components/parameters/Accept'
      responses:
        '200':
          description: Флаг сброшен, возвращается обновлённый объект списка.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ContactList' }
        '401': { $ref: '#/components/responses/401' }
        '404': { $ref: '#/components/responses/404' }
        '429': { $ref: '#/components/responses/429' }
        '500': { $ref: '#/components/responses/500' }

  # ─────────────────────────────────────────
  # КОНТАКТЫ
  # ─────────────────────────────────────────

  /contacts/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema: { type: integer }
        description: ID контакта
    get:
      summary: Показать контакт
      operationId: get-contacts-id
      tags: [Контакты]
      description: Возвращает информацию о конкретном контакте по его ID.
      parameters:
        - $ref: '#/components/parameters/ContentType'
        - $ref: '#/components/parameters/Accept'
      responses:
        '200':
          description: Объект контакта.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Contact' }
              example:
                id: 49
                list_id: 73
                number: "+77010000000"
                extra: "Иванов И.И."
                created_at: "2024-03-31 05:55:19"
                updated_at: "2024-03-31 05:55:19"
        '401': { $ref: '#/components/responses/401' }
        '404': { $ref: '#/components/responses/404' }
        '429': { $ref: '#/components/responses/429' }
    delete:
      summary: Удалить контакт
      operationId: delete-contacts-id
      tags: [Контакты]
      description: Удаляет контакт из списка контактов.
      parameters:
        - $ref: '#/components/parameters/ContentType'
        - $ref: '#/components/parameters/Accept'
      responses:
        '204':
          description: Контакт удалён.
        '401': { $ref: '#/components/responses/401' }
        '404': { $ref: '#/components/responses/404' }
        '422': { $ref: '#/components/responses/422' }
        '429': { $ref: '#/components/responses/429' }
        '500': { $ref: '#/components/responses/500' }

  # ─────────────────────────────────────────
  # АУДИОЗАПИСИ
  # ─────────────────────────────────────────

  /sounds/voices:
    get:
      summary: Получить список голосов синтеза
      operationId: get-synthesis-voices
      tags: [Аудиозаписи]
      description: |
        Голоса, доступные в поле `voice` при синтезе речи.

        У каждого голоса есть `sample_url` — прямая ссылка на короткий сэмпл (m4a), по которой
        его можно послушать перед выбором.

        Поле `realtime` показывает, годится ли голос для элемента «Сказать текст» в сценарии:
        там синтез идёт **во время звонка**, поэтому подходят только быстрые голоса. Если
        указать голос с `realtime: false`, он будет автоматически заменён на быстрый — абонент
        иначе слушал бы несколько секунд тишины.
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  default:
                    type: string
                    description: Голос, который используется, если поле `voice` не передано
                    example: Sulafat
                  data:
                    type: array
                    items:
                      type: object
                      properties:
                        name:       { type: string, description: "Значение для поля `voice`", example: Sulafat }
                        title:      { type: string, description: Название в личном кабинете, example: Айгерим }
                        gender:     { type: string, enum: [male, female] }
                        languages:  { type: array, items: { type: string }, description: "Языки голоса (kk, ru, en)" }
                        realtime:   { type: boolean, description: Годится ли для озвучки во время звонка }
                        pitch:      { type: boolean, description: "Поддерживает ли голос параметр `pitch` (высота тона)" }
                        emotions:   { type: boolean, description: "Понимает ли пометки эмоций в тексте — см. описание поля `text`" }
                        sample_url: { type: string, format: uri, description: Ссылка на сэмпл голоса }
              example:
                default: Sulafat
                data:
                  - name: Sulafat
                    title: Айгерим
                    gender: female
                    languages: [kk, ru, en]
                    realtime: false
                    pitch: false
                    emotions: true
                    sample_url: https://autocall.kz/voices/tts-samples/Sulafat.m4a
                  - name: madi
                    title: Мади
                    gender: male
                    languages: [kk]
                    realtime: true
                    pitch: true
                    emotions: false
                    sample_url: https://autocall.kz/voices/tts-samples/madi.m4a
        '401': { $ref: '#/components/responses/401' }
        '429': { $ref: '#/components/responses/429' }
        '500': { $ref: '#/components/responses/500' }

  /sounds:
    get:
      summary: Получить все аудиозаписи
      operationId: get-sounds
      tags: [Аудиозаписи]
      description: Возвращает все аудиозаписи аккаунта с постраничным выводом.
      parameters:
        - $ref: '#/components/parameters/ContentType'
        - $ref: '#/components/parameters/Accept'
        - $ref: '#/components/parameters/Page'
      responses:
        '200':
          description: Постраничный вывод аудиозаписей.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Pagination'
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: '#/components/schemas/Sound'
        '401': { $ref: '#/components/responses/401' }
        '429': { $ref: '#/components/responses/429' }

    post:
      summary: Создать аудиозапись
      operationId: post-sounds
      tags: [Аудиозаписи]
      description: |
        Создаёт аудиозапись двумя способами:
        - `synthesis` — синтез речи из текста
        - `upload` — загрузка файла mp3/wav/ogg в формате Base64
      parameters:
        - $ref: '#/components/parameters/ContentType'
        - $ref: '#/components/parameters/Accept'
      requestBody:
        required: true
        description: Параметры новой аудиозаписи.
        content:
          application/json:
            schema:
              type: object
              required: [name, type, data]
              properties:
                name:
                  type: string
                  minLength: 1
                  maxLength: 40
                  description: Название аудиозаписи
                type:
                  type: string
                  enum: [synthesis, upload]
                  description: Тип — синтез речи или загрузка файла
                data:
                  type: array
                  minItems: 1
                  maxItems: 1
                  description: Массив с одним объектом параметров аудио
                  items:
                    type: object
                    properties:
                      text:
                        type: string
                        minLength: 1
                        maxLength: 400
                        description: >
                          Если `type=synthesis`: текст для синтеза (обязательно).


                          У голосов с `emotions: true` в тексте можно ставить подсказку интонации в
                          квадратных скобках — она задаёт, КАК произнести следующую фразу, и вслух не
                          читается: `Здравствуйте! [радостно] Ваш заказ уже в пути.`
                          Подойдёт почти любое слово: радостно, грустно, шёпотом, устало, строго, смеюсь.
                          У голосов с `emotions: false` такие пометки вырезаются перед синтезом —
                          текст озвучится без них, ошибки не будет.
                      voice:
                        type: string
                        default: Sulafat
                        description: >
                          Если `type=synthesis`: голос для синтеза.
                          Полный список с прослушиванием — в [`GET /sounds/voices`](#/paths/~1sounds~1voices/get):
                          там у каждого голоса есть `sample_url`, по которой сэмпл можно послушать.
                          Список намеренно не продублирован здесь — голоса добавляются и выключаются,
                          и копия в документации протухла бы к следующему релизу.
                          Если поле не передано, используется `Sulafat` (женский, казахский/русский/английский).
                      speed:
                        type: number
                        default: 1
                        minimum: 0.25
                        maximum: 4
                        description: "Если type=synthesis: скорость речи"
                      pitch:
                        type: number
                        default: 0
                        minimum: -20
                        maximum: 20
                        description: >
                          Если `type=synthesis`: высота тона.
                          Работает только у голосов с `pitch: true` в `GET /sounds/voices`.
                          Новые голоса такого параметра не имеют, и для них значение игнорируется.
                      file:
                        type: string
                        description: "Если type=upload: файл в формате Base64 (mp3/wav/ogg, обязательно)"
                client_id:
                  type: string
                  maxLength: 60
                  description: ID в Вашей системе
            examples:
              Синтез речи:
                value:
                  name: "Уведомление об оплате"
                  type: synthesis
                  data:
                    - text: "Здравствуйте! Напоминаем об оплате до конца месяца."
                      voice: Sulafat
                      speed: 1
              Загрузка файла:
                value:
                  name: "Моё приветствие"
                  type: upload
                  data:
                    - file: "base64_строка_файла_mp3"
      responses:
        '201':
          description: Аудиозапись создана и отправлена на модерацию.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Sound' }
        '401': { $ref: '#/components/responses/401' }
        '402':
          description: Недостаточно средств для синтеза речи.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorMessage' }
        '422': { $ref: '#/components/responses/422' }
        '429': { $ref: '#/components/responses/429' }
        '500': { $ref: '#/components/responses/500' }

  /sounds/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema: { type: integer }
        description: ID аудиозаписи
    get:
      summary: Показать аудиозапись
      operationId: get-sounds-id
      tags: [Аудиозаписи]
      description: Возвращает детальную информацию об аудиозаписи.
      parameters:
        - $ref: '#/components/parameters/ContentType'
        - $ref: '#/components/parameters/Accept'
      responses:
        '200':
          description: Объект аудиозаписи.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Sound' }
        '401': { $ref: '#/components/responses/401' }
        '404': { $ref: '#/components/responses/404' }
        '429': { $ref: '#/components/responses/429' }
    put:
      summary: Редактировать аудиозапись
      operationId: put-sounds-id
      tags: [Аудиозаписи]
      description: Изменяет название или client_id аудиозаписи.
      parameters:
        - $ref: '#/components/parameters/ContentType'
        - $ref: '#/components/parameters/Accept'
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                  minLength: 1
                  maxLength: 40
                client_id:
                  type: string
                  maxLength: 60
      responses:
        '200':
          description: Обновлённый объект аудиозаписи.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Sound' }
        '401': { $ref: '#/components/responses/401' }
        '404': { $ref: '#/components/responses/404' }
        '422': { $ref: '#/components/responses/422' }
        '429': { $ref: '#/components/responses/429' }
        '500': { $ref: '#/components/responses/500' }
    delete:
      summary: Удалить аудиозапись
      operationId: delete-sounds-id
      tags: [Аудиозаписи]
      description: Удаляет аудиозапись из аккаунта.
      parameters:
        - $ref: '#/components/parameters/ContentType'
        - $ref: '#/components/parameters/Accept'
      responses:
        '204':
          description: Аудиозапись удалена.
        '401': { $ref: '#/components/responses/401' }
        '404': { $ref: '#/components/responses/404' }
        '422': { $ref: '#/components/responses/422' }
        '429': { $ref: '#/components/responses/429' }
        '500': { $ref: '#/components/responses/500' }

  /sounds/listen/{file}:
    parameters:
      - name: file
        in: path
        required: true
        schema: { type: string }
        description: Имя аудиофайла из поля `data` интерактивных обзвонов
    get:
      summary: Получить запись из интерактивного обзвона
      operationId: get-sounds-listen-file
      tags: [Аудиозаписи]
      description: |
        Доступ к записям голосовых ответов абонентов в интерактивных обзвонах (элемент типа `record`).

        В зависимости от параметра `type`:
        - `download` — сервер вернёт файл для скачивания
        - `text` — сервер вернёт JSON с распознанным текстом (тарифицируется)
      parameters:
        - $ref: '#/components/parameters/ContentType'
        - $ref: '#/components/parameters/Accept'
        - name: type
          in: query
          required: true
          schema:
            type: string
            enum: [download, text]
          description: Способ получения записи
      responses:
        '200':
          description: Файл для скачивания или объект с текстом.
          content:
            application/json:
              schema:
                type: object
                required: [text]
                properties:
                  text:
                    type: [string, "null"]
                    description: Распознанный текст (только при type=text)
        '401': { $ref: '#/components/responses/401' }
        '402':
          description: Недостаточно средств для распознавания речи.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorMessage' }
        '404': { $ref: '#/components/responses/404' }
        '422': { $ref: '#/components/responses/422' }
        '429': { $ref: '#/components/responses/429' }

  # ─────────────────────────────────────────
  # СЦЕНАРИИ
  # ─────────────────────────────────────────

  /scenarios:
    get:
      summary: Получить все сценарии
      operationId: get-scenarios
      tags: [Сценарии]
      description: Возвращает все сценарии аккаунта с постраничным выводом.
      parameters:
        - $ref: '#/components/parameters/ContentType'
        - $ref: '#/components/parameters/Accept'
        - $ref: '#/components/parameters/Page'
      responses:
        '200':
          description: Постраничный вывод сценариев.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Pagination'
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: '#/components/schemas/Scenario'
        '401': { $ref: '#/components/responses/401' }
        '429': { $ref: '#/components/responses/429' }

    post:
      summary: Создать сценарий
      operationId: post-scenarios
      tags: [Сценарии]
      description: Создаёт новый интерактивный сценарий для обзвонов. Сценарий передаётся как JSON-строка в параметре `data`.
      parameters:
        - $ref: '#/components/parameters/ContentType'
        - $ref: '#/components/parameters/Accept'
      requestBody:
        required: true
        description: Параметры нового сценария.
        content:
          application/json:
            schema:
              type: object
              required: [name, data]
              properties:
                name:
                  type: string
                  minLength: 1
                  maxLength: 40
                  description: Название сценария
                data:
                  type: string
                  description: |
                    Граф сценария — JSON-объект, **сериализованный в строку** (`json_encode` в PHP,
                    `JSON.stringify` в JS, `json.dumps` в Python). Ключи объекта = id элементов.

                    📖 **Полный справочник структуры, всех типов элементов и три готовых примера —
                    в описании раздела «Сценарии» выше.** Ниже — минимальный рабочий сценарий
                    (объект ДО сериализации):
                    ```json
                    {
                      "START":       { "id": "START", "type": "start", "name": "Начало", "next": "TALK_DETECT" },
                      "TALK_DETECT": { "id": "TALK_DETECT", "type": "talk_detect", "name": "Ждём речи", "next": "a1b2c3" },
                      "a1b2c3":      { "id": "a1b2c3", "type": "playback", "name": "Приветствие", "next": "END", "data": 12345 },
                      "END":         { "id": "END", "type": "end", "name": "Конец", "next": null }
                    }
                    ```
                    `data: 12345` у `playback` — id Вашей аудиозаписи (см. раздел «Аудиозаписи»).

                    Доступные типы: `start`, `talk_detect`, `end`, `playback`, `record`, `dtmf`,
                    `speech`, `dial`, `sms`, `sms_me`, `request`, `say`, `stt_var`, `setvar`,
                    `ifelse`, `switchvar` — те же, что и в конструкторе на сайте.
                client_id:
                  type: string
                  maxLength: 60
                  description: ID в Вашей системе
      responses:
        '201':
          description: Сценарий создан и отправлен на модерацию.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Scenario' }
        '401': { $ref: '#/components/responses/401' }
        '422': { $ref: '#/components/responses/422' }
        '429': { $ref: '#/components/responses/429' }
        '500': { $ref: '#/components/responses/500' }

  /scenarios/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema: { type: integer }
        description: ID сценария
    get:
      summary: Показать сценарий
      operationId: get-scenarios-id
      tags: [Сценарии]
      description: Возвращает детальную информацию о сценарии, включая его структуру.
      parameters:
        - $ref: '#/components/parameters/ContentType'
        - $ref: '#/components/parameters/Accept'
      responses:
        '200':
          description: Объект сценария с полной структурой.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ScenarioDetail' }
        '401': { $ref: '#/components/responses/401' }
        '404': { $ref: '#/components/responses/404' }
        '429': { $ref: '#/components/responses/429' }
    put:
      summary: Редактировать сценарий
      operationId: put-scenarios-id
      tags: [Сценарии]
      description: |
        Изменяет название, структуру или client_id сценария. Передавайте только те поля,
        которые меняете.

        ⚠️ `data` здесь — такая же **строка** с закодированным JSON, как при создании.
        А `GET /scenarios/{id}` отдаёт `data` уже **объектом**: чтобы изменить существующий
        сценарий, разберите ответ, поправьте нужный элемент и сериализуйте обратно.
        Структура графа целиком — в описании раздела «Сценарии».
      parameters:
        - $ref: '#/components/parameters/ContentType'
        - $ref: '#/components/parameters/Accept'
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                name: { type: string, minLength: 1, maxLength: 40 }
                data:
                  type: string
                  description: Новый граф сценария, сериализованный в строку. Заменяет структуру целиком.
                client_id: { type: string, maxLength: 60 }
      responses:
        '200':
          description: Обновлённый объект сценария.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ScenarioDetail' }
        '401': { $ref: '#/components/responses/401' }
        '404': { $ref: '#/components/responses/404' }
        '422': { $ref: '#/components/responses/422' }
        '429': { $ref: '#/components/responses/429' }
        '500': { $ref: '#/components/responses/500' }
    delete:
      summary: Удалить сценарий
      operationId: delete-scenarios-id
      tags: [Сценарии]
      description: Удаляет сценарий из аккаунта.
      parameters:
        - $ref: '#/components/parameters/ContentType'
        - $ref: '#/components/parameters/Accept'
      responses:
        '204':
          description: Сценарий удалён.
        '401': { $ref: '#/components/responses/401' }
        '404': { $ref: '#/components/responses/404' }
        '422': { $ref: '#/components/responses/422' }
        '429': { $ref: '#/components/responses/429' }
        '500': { $ref: '#/components/responses/500' }

  # ─────────────────────────────────────────
  # ИИ-АГЕНТЫ
  # ─────────────────────────────────────────

  /agents:
    get:
      summary: Получить всех ИИ-агентов
      operationId: get-agents
      tags: [ИИ-агенты]
      description: |
        Возвращает всех агентов аккаунта с постраничным выводом.

        #### Статусы агента
        | Статус | Значение | Можно в обзвон |
        |--------|----------|----------------|
        | `generating` | считаются параметры агента | нет |
        | `ready` | готов | да |
        | `failed` | не удалось подготовить | нет |
        | `draft` | черновик из мастера в личном кабинете | нет |
      parameters:
        - $ref: '#/components/parameters/ContentType'
        - $ref: '#/components/parameters/Accept'
        - $ref: '#/components/parameters/ClientIdFilter'
        - $ref: '#/components/parameters/Page'
        - name: status
          in: query
          required: false
          schema:
            type: string
            enum: [draft, generating, ready, failed]
          description: Оставить только агентов с этим статусом.
      responses:
        '200':
          description: Постраничный вывод агентов.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Pagination'
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: '#/components/schemas/Agent' }
        '401': { $ref: '#/components/responses/401' }
        '404': { $ref: '#/components/responses/404' }
        '429': { $ref: '#/components/responses/429' }

    post:
      summary: Создать ИИ-агента
      operationId: post-agents
      tags: [ИИ-агенты]
      description: |
        Создаёт голосового ИИ-агента. В ответе агент со статусом `generating` —
        дождитесь `ready` (опрашивайте `GET /agents/{id}`), прежде чем ставить его в обзвон.

        #### Из чего состоит агент
        - **`prompt`** — сценарий разговора обычным текстом. Обязан содержать три секции,
          см. ниже. Правила безопасности мы дописываем сами — их указывать не нужно.
        - **`data_schema`** — что агент должен вынести из разговора. Эти поля попадут
          в результат звонка и в выгрузку обзвона.
        - **`tools`** — необязательные действия по ходу разговора: отправить SMS (`sms`)
          или сходить запросом на ваш сервер и озвучить ответ (`http`).
        - **`voice`** — голос агента, из `GET /agents/voices`.
        - **`max_seconds`** — потолок длительности разговора. Он же потолок стоимости звонка.

        #### Обязательные секции `prompt`
        Сценарий должен содержать три секции — иначе `422`:

        | Секция | Что в ней |
        |--------|-----------|
        | `## Кто ты` | Имя агента, от лица какой компании звонит, характер в 2–4 словах |
        | `## Задача звонка` | Чего добиться, 1–2 предложения, конкретно |
        | `## Ход разговора` | Шаги разговора. Первым шагом — дословная первая фраза |

        Секция — это заголовок `## Название` (можно глубже: `###`) и **непустой** текст под ним.
        Регистр и лишние пробелы не важны, порядок секций — любой.

        Дальше по желанию — `## Что собрать`, `## Инструменты`, `## Границы` (чего не обещать,
        что отвечать на неудобные вопросы) и любые свои секции.

        ⚠️ Чем длиннее `prompt`, тем дороже каждый звонок: сценарий заново отправляется модели
        на каждой реплике. Пишите плотно, без воды и повторов.
      parameters:
        - $ref: '#/components/parameters/ContentType'
        - $ref: '#/components/parameters/Accept'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/AgentCreate' }
            examples:
              Простой агент — подтверждение записи:
                value:
                  name: "Асель"
                  voice: "Sulafat"
                  max_seconds: 180
                  prompt: |
                    ## Кто ты
                    Асель — женский голос стоматологии «Дента». Звонишь пациенту подтвердить
                    запись на приём. Характер: спокойная, вежливая, без напора.

                    ## Задача звонка
                    Узнать, придёт ли пациент в назначенное время. Если не придёт — предложить
                    перенести на другой день и записать желаемую дату.

                    ## Ход разговора
                    1. Первая фраза РОВНО: «Здравствуйте! Это Асель, стоматология Дента.
                       Вы записаны к нам на приём — подскажите, всё в силе?»
                    2. Подтвердил — поблагодари, напомни время и попрощайся.
                    3. Не придёт — спроси, на какой день перенести, повтори дату вслух.
                    4. Просит перезвонить позже — согласись и заверши разговор.

                    ## Границы
                    Про цены: консультация бесплатная, стоимость лечения врач назовёт на приёме.
                    Не ставь диагнозов и не обещай скидок.
                  data_schema:
                    - { key: "confirmed", label: "Подтвердил приём" }
                    - { key: "new_date", label: "Просит перенести на" }
                    - { key: "comment", label: "Комментарий" }
              Агент с SMS и запросом на ваш сервер:
                value:
                  name: "Ерлан"
                  voice: "Charon"
                  max_seconds: 300
                  client_id: "sales-agent-v2"
                  prompt: |
                    ## Кто ты
                    Ерлан — мужской голос интернет-магазина «Технодом». Звонишь клиенту,
                    который оставил заявку на сайте. Характер: бодрый, деловой.

                    ## Задача звонка
                    Выяснить, актуален ли заказ, и ответить на вопросы клиента по нему.

                    ## Ход разговора
                    1. Первая фраза РОВНО: «Здравствуйте! Ерлан, магазин Технодом.
                       Вы оставляли заявку — вам ещё актуально?»
                    2. Актуально — уточни, есть ли вопросы, и передай заказ в работу.
                    3. Спрашивает про доставку — скажи «секундочку, посмотрю» и проверь статус.
                    4. Просит прайс — предложи отправить SMS и отправь.

                    ## Границы
                    Сроки доставки называй только те, что вернул сервер. Скидок не обещай.
                  data_schema:
                    - { key: "interested", label: "Заказ актуален" }
                    - { key: "question", label: "Вопрос клиента" }
                  tools:
                    - type: sms
                      name: "price"
                      when: "клиент попросил прислать прайс"
                      text: "Прайс-лист: https://example.kz/price"
                    - type: http
                      name: "delivery"
                      when: "клиент спрашивает о статусе доставки"
                      method: GET
                      url: "https://api.example.kz/delivery?phone={{phone}}"
                      headers:
                        - { key: "Authorization", value: "Bearer ВАШ_ТОКЕН" }
                      bodyType: none
                      response_template: "Ваш заказ {{status}}, ожидайте {{eta}}."
                      fallback: "Не получилось проверить прямо сейчас, перезвоним."
      responses:
        '201':
          description: Агент создан, идёт подготовка (`status=generating`).
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Agent' }
        '401': { $ref: '#/components/responses/401' }
        '422': { $ref: '#/components/responses/422' }
        '429': { $ref: '#/components/responses/429' }
        '500': { $ref: '#/components/responses/500' }

  /agents/voices:
    get:
      summary: Получить список голосов
      operationId: get-agent-voices
      tags: [ИИ-агенты]
      description: |
        Доступные голоса агентов. Значение `name` передаётся в поле `voice` при создании агента,
        `sample` — ссылка на аудиофрагмент для прослушивания.

        Пол голоса важен: агент говорит о себе в первом лице, поэтому `prompt` пишите
        в роде выбранного голоса.
      parameters:
        - $ref: '#/components/parameters/ContentType'
        - $ref: '#/components/parameters/Accept'
      responses:
        '200':
          description: Список голосов.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/Voice' }
        '401': { $ref: '#/components/responses/401' }
        '429': { $ref: '#/components/responses/429' }

  /agents/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema: { type: integer }
        description: ID агента.
    get:
      summary: Получить ИИ-агента
      operationId: get-agent
      tags: [ИИ-агенты]
      description: Возвращает агента по ID. Этим же запросом опрашивают готовность (`status=ready`).
      parameters:
        - $ref: '#/components/parameters/ContentType'
        - $ref: '#/components/parameters/Accept'
      responses:
        '200':
          description: Агент.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Agent' }
        '401': { $ref: '#/components/responses/401' }
        '404': { $ref: '#/components/responses/404' }
        '429': { $ref: '#/components/responses/429' }

    put:
      summary: Изменить ИИ-агента
      operationId: put-agent
      tags: [ИИ-агенты]
      description: |
        Меняет только переданные поля. `data_schema` и `tools` при передаче заменяются
        целиком — чтобы удалить поле или инструмент, пришлите массив без него.

        Изменение `prompt` или `max_seconds` возвращает агента в `generating`: параметры
        считаются заново. Уже созданные обзвоны это не затрагивает — они звонят по слепку
        агента, снятому при создании обзвона.

        `active: false` временно выводит агента из оборота, не удаляя его.
      parameters:
        - $ref: '#/components/parameters/ContentType'
        - $ref: '#/components/parameters/Accept'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/AgentUpdate' }
            examples:
              Поправить сценарий:
                value:
                  prompt: |
                    ## Кто ты
                    Асель — женский голос стоматологии «Дента». Характер: спокойная, вежливая.

                    ## Задача звонка
                    Подтвердить запись пациента на приём.

                    ## Ход разговора
                    1. Первая фраза РОВНО: «Здравствуйте! Это Асель из стоматологии Дента.»
                    2. Уточни, придёт ли пациент в назначенное время.
              Временно отключить:
                value:
                  active: false
      responses:
        '200':
          description: Агент изменён.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Agent' }
        '401': { $ref: '#/components/responses/401' }
        '404': { $ref: '#/components/responses/404' }
        '422': { $ref: '#/components/responses/422' }
        '429': { $ref: '#/components/responses/429' }
        '500': { $ref: '#/components/responses/500' }

    delete:
      summary: Удалить ИИ-агента
      operationId: delete-agent
      tags: [ИИ-агенты]
      description: |
        Удаляет агента. Отчёты по уже завершённым обзвонам с ним сохраняются.

        Агента, занятого в незавершённом обзвоне (`running`, `awaiting`, `moderation`),
        удалить нельзя — дождитесь окончания обзвона или остановите его.
      parameters:
        - $ref: '#/components/parameters/ContentType'
        - $ref: '#/components/parameters/Accept'
      responses:
        '204':
          description: Агент удалён.
        '401': { $ref: '#/components/responses/401' }
        '403':
          description: Агент занят в незавершённом обзвоне.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorMessage' }
        '404': { $ref: '#/components/responses/404' }
        '429': { $ref: '#/components/responses/429' }
        '500': { $ref: '#/components/responses/500' }

  # ─────────────────────────────────────────
  # ОБЗВОНЫ
  # ─────────────────────────────────────────

  /autocalls:
    get:
      summary: Получить все обзвоны
      operationId: get-autocalls
      tags: [Обзвоны]
      description: |
        Возвращает все обзвоны аккаунта с постраничным выводом.

        #### Статусы обзвонов
        | Статус | Значение | Окончательный |
        |--------|----------|---------------|
        | `running` | выполняется | нет |
        | `completed` | завершён успешно | да |
        | `awaiting` | ожидает своего времени | нет |
        | `moderation` | на модерации | нет |
        | `denied` | запрещён модераторами | да |
      parameters:
        - $ref: '#/components/parameters/ContentType'
        - $ref: '#/components/parameters/Accept'
        - $ref: '#/components/parameters/DateFrom'
        - $ref: '#/components/parameters/DateTo'
        - $ref: '#/components/parameters/Order'
        - $ref: '#/components/parameters/ClientIdFilter'
        - $ref: '#/components/parameters/Page'
      responses:
        '200':
          description: Постраничный вывод обзвонов.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Pagination'
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: '#/components/schemas/AutocallSummary'
        '401': { $ref: '#/components/responses/401' }
        '429': { $ref: '#/components/responses/429' }

    post:
      summary: Создать обзвон
      operationId: post-autocalls
      tags: [Обзвоны]
      description: |
        Создаёт новый обзвон. Поддерживает три типа:
        - `regular` — обычный обзвон с аудиозаписью (нужен `audio_id`)
        - `interactive` — интерактивный обзвон по сценарию (нужен `scenario_id`)
        - `agent` — живой разговор с голосовым ИИ-агентом (нужен `agent_id`)

        В `list_id` можно передавать как ID существующих списков, так и объекты с номерами напрямую (будет создан автоматический список).
        В `audio_id` можно передавать как ID существующих аудиозаписей, так и объекты для создания новых (синтез или загрузка).

        Если общее количество номеров ≤ 5000 — ответ `201` (создан сразу).
        Если > 5000 — ответ `202` (создаётся в фоне).
      parameters:
        - $ref: '#/components/parameters/ContentType'
        - $ref: '#/components/parameters/Accept'
      requestBody:
        required: true
        description: Параметры нового обзвона.
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AutocallCreate'
            examples:
              Обычный обзвон:
                value:
                  name: "Уведомление об акции"
                  type: regular
                  time_from: "10:00:00"
                  time_to: "20:00:00"
                  audio_id: [5]
                  list_id: [10]
              Без создания списка — номера прямо в запросе:
                summary: Создавать список контактов заранее НЕ нужно — передайте номера в list_id
                description: |
                  Не обязательно сначала вызывать `POST /lists`. Можно передать номера
                  напрямую в `list_id` как массив объектов `{ "number": "+7..." }` —
                  система сама создаст временный список. То же работает для `audio_id`
                  (можно передать существующий ID или объект для синтеза/загрузки).
                value:
                  name: "Уведомление об акции"
                  type: regular
                  time_from: "10:00:00"
                  time_to: "20:00:00"
                  audio_id: [5]
                  list_id:
                    - number: "+77010000001"
                    - number: "+77010000002"
                    - number: "+77010000003"
              Обзвон ИИ-агентом:
                summary: Живой разговор — агент сам ведёт диалог и заполняет поля результата
                description: |
                  Агент должен быть в статусе `ready` (см. `GET /agents`). Переменные контакта
                  (`variables`) агент знает во время разговора и обращается к абоненту по имени.

                  Аудиозаписи не нужны — весь разговор ведёт агент.
                value:
                  name: "Подтверждение записи на приём"
                  type: agent
                  agent_id: 42
                  time_from: "10:00:00"
                  time_to: "19:00:00"
                  max_tries: 2
                  tries_interval: 60
                  record: true
                  list_id:
                    - number: "+77010000001"
                      variables: { name: "Иван", time: "14:30" }
                    - number: "+77010000002"
                      variables: { name: "Пётр", time: "16:00" }
                  callback_url: "https://example.com/reports"
              Интерактивный обзвон:
                value:
                  name: "Опрос удовлетворённости"
                  type: interactive
                  time_from: "10:00:00"
                  time_to: "18:00:00"
                  scenario_id: 512
                  list_id: [15]
                  max_tries: 3
                  tries_interval: 60
                  record: true
                  callback_url: "https://example.com/reports"
              С новыми номерами и синтезом:
                value:
                  name: "Код подтверждения"
                  type: regular
                  time_from: "00:00:00"
                  time_to: "23:59:59"
                  audio_id:
                    - type: synthesis
                      data:
                        text: "Ваш код подтверждения: 3, 0, 7, 5."
                        voice: Sulafat
                  list_id:
                    - number: "+77010000000"
                  sms_unanswered: true
                  text_unanswered: "Ваш код: 3075"
              С персональными SMS недозвонившимся:
                summary: Переменные подставляются в SMS для тех, кто не ответил
                value:
                  name: "Напоминание о записи"
                  type: regular
                  time_from: "10:00:00"
                  time_to: "20:00:00"
                  audio_id: [5]
                  list_id:
                    - number: "+77010000001"
                      variables: { name: "Иван", time: "14:30" }
                    - number: "+77010000002"
                      variables: { name: "Пётр", time: "16:00" }
                  sms_unanswered: true
                  text_unanswered: "{{name}}, мы не дозвонились. Ждём вас в {{time}}."
      responses:
        '201':
          description: Обзвон создан (до 5000 номеров).
          content:
            application/json:
              schema: { $ref: '#/components/schemas/AutocallDetail' }
        '202':
          description: Обзвон принят в обработку (более 5000 номеров, создаётся в фоне).
        '401': { $ref: '#/components/responses/401' }
        '403':
          description: Обзвон отклонён из-за подозрения в спаме (слишком много номеров с короткой аудиозаписью).
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ValidationError' }
        '422': { $ref: '#/components/responses/422' }
        '429': { $ref: '#/components/responses/429' }

  /autocalls/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema: { type: integer }
        description: ID обзвона
    get:
      summary: Показать обзвон
      operationId: get-autocalls-id
      tags: [Обзвоны]
      description: |
        Возвращает детальную информацию об обзвоне, включая статистику и постраничный список всех звонков.

        #### Статусы звонков
        | Статус | Значение | Окончательный |
        |--------|----------|---------------|
        | `in_queue` | в очереди | нет |
        | `success` | успешно | да |
        | `recalled` | перезвонил сам | да |
        | `no_answer` | нет ответа | да |
        | `busy` | занято | да |
        | `error` | не существует | да |
        | `spam` | пожаловался на спам | да |
      parameters:
        - $ref: '#/components/parameters/ContentType'
        - $ref: '#/components/parameters/Accept'
        - $ref: '#/components/parameters/Page'
      responses:
        '200':
          description: Детальный объект обзвона со статистикой и звонками.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/AutocallDetail' }
        '401': { $ref: '#/components/responses/401' }
        '404': { $ref: '#/components/responses/404' }
        '429': { $ref: '#/components/responses/429' }
    put:
      summary: Редактировать обзвон
      operationId: put-autocalls-id
      tags: [Обзвоны]
      description: |
        Изменяет параметры обзвона. Доступно только для обзвонов в статусах `awaiting` и `moderation`.
        Редактируются только перечисленные ниже поля.
      parameters:
        - $ref: '#/components/parameters/ContentType'
        - $ref: '#/components/parameters/Accept'
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                name: { type: string, minLength: 1, maxLength: 40 }
                time_from: { type: string, minLength: 8, maxLength: 8 }
                time_to: { type: string, minLength: 8, maxLength: 8 }
                tries_interval:
                  type: integer
                  enum: [15, 30, 60, 120]
                date_from: { type: string, minLength: 10, maxLength: 10 }
                client_id: { type: string, maxLength: 60 }
                callback_url: { type: string, maxLength: 256 }
      responses:
        '200':
          description: Обновлённый объект обзвона.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/AutocallDetail' }
        '401': { $ref: '#/components/responses/401' }
        '404': { $ref: '#/components/responses/404' }
        '422': { $ref: '#/components/responses/422' }
        '429': { $ref: '#/components/responses/429' }
        '500': { $ref: '#/components/responses/500' }
    delete:
      summary: Удалить обзвон
      operationId: delete-autocalls-id
      tags: [Обзвоны]
      description: Удаляет обзвон. Нельзя удалить обзвон в статусе `running`.
      parameters:
        - $ref: '#/components/parameters/ContentType'
        - $ref: '#/components/parameters/Accept'
      responses:
        '204':
          description: Обзвон удалён.
        '401': { $ref: '#/components/responses/401' }
        '404': { $ref: '#/components/responses/404' }
        '422': { $ref: '#/components/responses/422' }
        '429': { $ref: '#/components/responses/429' }
        '500': { $ref: '#/components/responses/500' }

  /autocalls/calculate:
    post:
      summary: Узнать стоимость обзвона
      operationId: post-autocalls-calculate
      tags: [Обзвоны]
      description: Рассчитывает максимальную стоимость обзвона до его создания. Принимает те же параметры, что и создание обзвона.
      parameters:
        - $ref: '#/components/parameters/ContentType'
        - $ref: '#/components/parameters/Accept'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/AutocallCreate' }
      responses:
        '201':
          description: Максимальная стоимость обзвона.
          content:
            application/json:
              schema:
                type: object
                required: [max_cost]
                properties:
                  max_cost: { type: number }
              example:
                max_cost: 245.60
        '401': { $ref: '#/components/responses/401' }
        '403':
          description: Обзвон отклонён из-за подозрения в спаме.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ValidationError' }
        '422': { $ref: '#/components/responses/422' }
        '429': { $ref: '#/components/responses/429' }

  /autocalls/relaunch/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema: { type: integer }
        description: ID завершённого обзвона
    get:
      summary: Перезапустить обзвон
      operationId: get-autocalls-relaunch-id
      tags: [Обзвоны]
      description: |
        Создаёт новый список контактов из номеров завершённого обзвона, которые **не были успешно обзвонены**.
        Удобно для повторного обзвона недозвонившихся.
        Работает только для обзвонов со статусом `completed`.
      parameters:
        - $ref: '#/components/parameters/ContentType'
        - $ref: '#/components/parameters/Accept'
      responses:
        '201':
          description: Новый список контактов создан.
          content:
            application/json:
              schema:
                type: object
                required: [id, name, updated_at, created_at, used, contacts_count, importing]
                properties:
                  id: { type: integer, description: ID нового списка }
                  name: { type: string }
                  created_at: { type: string }
                  updated_at: { type: string }
                  used: { type: integer }
                  contacts_count: { type: integer }
                  importing: { type: boolean }
        '401': { $ref: '#/components/responses/401' }
        '403':
          description: Обзвон ещё не завершён.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ValidationError' }
        '404': { $ref: '#/components/responses/404' }
        '429': { $ref: '#/components/responses/429' }

  /calls/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema: { type: integer }
        description: ID звонка
    get:
      summary: Показать звонок
      operationId: get-calls-id
      tags: [Обзвоны]
      description: Возвращает детальную информацию о конкретном звонке по его ID.
      parameters:
        - $ref: '#/components/parameters/ContentType'
        - $ref: '#/components/parameters/Accept'
      responses:
        '200':
          description: Объект звонка.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Call' }
        '401': { $ref: '#/components/responses/401' }
        '404': { $ref: '#/components/responses/404' }
        '429': { $ref: '#/components/responses/429' }

  /calls/recordings/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema: { type: integer }
        description: ID звонка
    get:
      summary: Скачать запись разговора
      operationId: get-calls-recordings-id
      tags: [Обзвоны]
      description: |
        Возвращает аудиофайл записи разговора для указанного звонка.
        Записи доступны только если при создании обзвона был указан параметр `record: true`.
      parameters:
        - $ref: '#/components/parameters/ContentType'
        - $ref: '#/components/parameters/Accept'
      responses:
        '200':
          description: Аудиофайл записи разговора (бинарный контент).
        '401': { $ref: '#/components/responses/401' }
        '404': { $ref: '#/components/responses/404' }
        '429': { $ref: '#/components/responses/429' }

  /calls/{id}/transcript:
    parameters:
      - name: id
        in: path
        required: true
        schema: { type: integer }
        description: ID звонка
    get:
      summary: Расшифровка разговора
      operationId: get-calls-id-transcript
      tags: [Обзвоны]
      description: |
        Возвращает диалог абонента с ИИ-агентом — то же, что видно в личном кабинете.

        Доступна только для обзвонов типа `agent` и **не зависит** от параметра `record`:
        расшифровка снимается из разговора напрямую, а не из аудиозаписи. Для обычных и
        интерактивных обзвонов вернётся пустой `transcript`.

        Расшифровка появляется после завершения разговора и в течение нескольких секунд
        после него может уточняться (автоматическая правка коротких реплик абонента).
        Если забираете её по вебхуку — берите с небольшой задержкой либо перечитывайте.
      parameters:
        - $ref: '#/components/parameters/ContentType'
        - $ref: '#/components/parameters/Accept'
      responses:
        '200':
          description: Расшифровка разговора.
          content:
            application/json:
              schema:
                type: object
                properties:
                  call_id: { type: integer }
                  number: { type: string, description: Номер абонента }
                  transcript:
                    type: array
                    description: Реплики в порядке разговора.
                    items:
                      type: object
                      properties:
                        role:
                          type: string
                          enum: [agent, customer]
                          description: "`agent` — ИИ-агент, `customer` — абонент"
                        text: { type: string }
                  text:
                    type: string
                    description: Тот же диалог одним текстом, по реплике на строку.
              example:
                call_id: 12345
                number: "+77010000001"
                transcript:
                  - { role: agent, text: "Здравствуйте! Вам удобно говорить?" }
                  - { role: customer, text: "Да, слушаю" }
                text: "Агент: Здравствуйте! Вам удобно говорить?\nАбонент: Да, слушаю"
        '401': { $ref: '#/components/responses/401' }
        '404': { $ref: '#/components/responses/404' }
        '429': { $ref: '#/components/responses/429' }

  # ─────────────────────────────────────────
  # SMS РАССЫЛКИ
  # ─────────────────────────────────────────

  /bulks:
    get:
      summary: Получить все рассылки
      operationId: get-bulks
      tags: [SMS рассылки]
      description: Возвращает все SMS рассылки аккаунта с постраничным выводом.
      parameters:
        - $ref: '#/components/parameters/ContentType'
        - $ref: '#/components/parameters/Accept'
        - $ref: '#/components/parameters/DateFrom'
        - $ref: '#/components/parameters/DateTo'
        - $ref: '#/components/parameters/Order'
        - $ref: '#/components/parameters/ClientIdFilter'
        - $ref: '#/components/parameters/Page'
      responses:
        '200':
          description: Постраничный вывод рассылок.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Pagination'
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: '#/components/schemas/BulkSummary'
        '401': { $ref: '#/components/responses/401' }
        '429': { $ref: '#/components/responses/429' }

    post:
      summary: Создать рассылку
      operationId: post-bulks
      tags: [SMS рассылки]
      description: |
        Создаёт новую SMS рассылку.
        В `list_id` можно передавать ID существующих списков или объекты с номерами напрямую (максимум 5000).
        Для большего количества — сначала создайте список контактов через `/lists`.

        #### Персонализация через переменные `{{varname}}`
        В тексте рассылки можно использовать переменные контактов вида `{{name}}`, `{{debt}}` и т.п.
        При отправке каждая переменная заменяется на значение из поля `variables` соответствующего контакта.
        Это позволяет отправить персональные сообщения тысячам получателей **одним** запросом.

        Пример текста: `Здравствуйте, {{name}}! Ваш долг {{debt}} тенге. Оплатите!`

        Переменные берутся из контактов, у которых они заданы — либо в существующем списке
        (см. `POST /lists` и `POST /lists/append/{id}`), либо в объектах номеров, переданных в `list_id` напрямую.
        Неизвестная переменная (для которой у контакта нет значения) заменяется на пустую строку.

        > ⚠️ Стоимость и количество SMS-сегментов рассчитываются по «худшему случаю» — берётся
        > самое длинное значение каждой переменной среди всех контактов. Помните, что кириллица
        > использует Unicode-кодировку (до 70 символов в сегменте против 160 для латиницы).
      parameters:
        - $ref: '#/components/parameters/ContentType'
        - $ref: '#/components/parameters/Accept'
      requestBody:
        required: true
        description: Параметры рассылки.
        content:
          application/json:
            schema:
              type: object
              required: [name, text, list_id]
              properties:
                name:
                  type: string
                  minLength: 1
                  maxLength: 40
                  description: Название рассылки
                text:
                  type: string
                  minLength: 1
                  maxLength: 1600
                  description: |
                    Текст SMS сообщения. Поддерживает переменные вида `{{varname}}`, которые
                    заменяются на значения из поля `variables` каждого контакта.
                    Например: `Здравствуйте, {{name}}! Ваш долг {{debt}} тенге.`
                list_id:
                  type: array
                  minItems: 1
                  maxItems: 5000
                  description: ID списков контактов и/или объекты с номерами
                  items:
                    oneOf:
                      - type: integer
                        description: ID существующего списка
                      - type: object
                        required: [number]
                        properties:
                          number: { type: string, minLength: 12, maxLength: 12 }
                          extra: { type: string, maxLength: 60 }
                          variables:
                            type: object
                            description: |
                              Переменные контакта для подстановки в текст через `{{varname}}`.
                              Ключи — `[a-zA-Z0-9_]` (до 30 символов), значения — строки (до 255 символов).
                            additionalProperties: { type: string }
                            example: { name: "Иван", debt: "15000" }
                date_from:
                  type: string
                  minLength: 10
                  maxLength: 10
                  description: "День запуска в формате Y-m-d (по умолчанию: сегодня)"
                time_from:
                  type: string
                  minLength: 8
                  maxLength: 8
                  description: "Время запуска в формате H:i:s (по умолчанию: сейчас)"
                client_id:
                  type: string
                  maxLength: 60
                  description: ID в Вашей системе
                callback_url:
                  type: string
                  maxLength: 256
                  description: URL для доставки отчётов о рассылке и сообщениях
            examples:
              Простая рассылка:
                value:
                  name: "Акция — ноябрь"
                  text: "Скидка 20% только сегодня! Промокод: SALE20"
                  list_id: [10, 15]
                  time_from: "10:00:00"
                  callback_url: "https://example.com/sms-reports"
              Без создания списка — номера прямо в запросе:
                summary: Создавать список контактов заранее НЕ нужно — передайте номера в list_id
                description: |
                  Не обязательно сначала вызывать `POST /lists`. Можно передать номера
                  напрямую в `list_id` как массив объектов `{ "number": "+7..." }` —
                  система сама создаст временный список. Удобно для разовых рассылок.
                value:
                  name: "Акция — ноябрь"
                  text: "Скидка 20% только сегодня! Промокод: SALE20"
                  list_id:
                    - number: "+77010000001"
                    - number: "+77010000002"
                    - number: "+77010000003"
              Персональная рассылка по существующему списку:
                summary: Текст с переменными, контакты берутся из списка
                value:
                  name: "Напоминание о долге"
                  text: "Здравствуйте, {{name}}! Ваш долг {{debt}} тенге. Оплатите до конца месяца."
                  list_id: [10]
              Персональная рассылка одним запросом:
                summary: Переменные и номера передаются прямо в запросе
                value:
                  name: "Напоминание о долге"
                  text: "Здравствуйте, {{name}}! Ваш долг {{debt}} тенге. Оплатите до конца месяца."
                  list_id:
                    - number: "+77010000001"
                      variables: { name: "Иван", debt: "15000" }
                    - number: "+77010000002"
                      variables: { name: "Пётр", debt: "3200" }
      responses:
        '201':
          description: Рассылка создана.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/BulkDetail' }
        '401': { $ref: '#/components/responses/401' }
        '422': { $ref: '#/components/responses/422' }
        '429': { $ref: '#/components/responses/429' }
        '500': { $ref: '#/components/responses/500' }

  /bulks/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema: { type: integer }
        description: ID рассылки
    get:
      summary: Показать рассылку
      operationId: get-bulks-id
      tags: [SMS рассылки]
      description: Возвращает детальную информацию о рассылке и постраничный список всех сообщений.
      parameters:
        - $ref: '#/components/parameters/ContentType'
        - $ref: '#/components/parameters/Accept'
        - $ref: '#/components/parameters/Page'
      responses:
        '200':
          description: Детальный объект рассылки.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/BulkDetail' }
        '401': { $ref: '#/components/responses/401' }
        '404': { $ref: '#/components/responses/404' }
        '429': { $ref: '#/components/responses/429' }
    put:
      summary: Редактировать рассылку
      operationId: put-bulks-id
      tags: [SMS рассылки]
      description: Изменяет параметры рассылки. Доступно только для рассылок в статусах `awaiting` и `moderation`.
      parameters:
        - $ref: '#/components/parameters/ContentType'
        - $ref: '#/components/parameters/Accept'
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                name: { type: string, minLength: 1, maxLength: 40 }
                time_from: { type: string, minLength: 8, maxLength: 8 }
                date_from: { type: string, minLength: 10, maxLength: 10 }
                client_id: { type: string, maxLength: 60 }
                callback_url: { type: string, maxLength: 256 }
      responses:
        '200':
          description: Обновлённый объект рассылки.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/BulkDetail' }
        '401': { $ref: '#/components/responses/401' }
        '404': { $ref: '#/components/responses/404' }
        '422': { $ref: '#/components/responses/422' }
        '429': { $ref: '#/components/responses/429' }
        '500': { $ref: '#/components/responses/500' }
    delete:
      summary: Удалить рассылку
      operationId: delete-bulks-id
      tags: [SMS рассылки]
      description: Удаляет рассылку. Нельзя удалить рассылку в статусе `running`.
      parameters:
        - $ref: '#/components/parameters/ContentType'
        - $ref: '#/components/parameters/Accept'
      responses:
        '204':
          description: Рассылка удалена.
        '401': { $ref: '#/components/responses/401' }
        '404': { $ref: '#/components/responses/404' }
        '422': { $ref: '#/components/responses/422' }
        '429': { $ref: '#/components/responses/429' }
        '500': { $ref: '#/components/responses/500' }

  /bulks/calculate:
    post:
      summary: Узнать стоимость рассылки
      operationId: post-bulks-calculate
      tags: [SMS рассылки]
      description: Рассчитывает максимальную стоимость рассылки до её создания. Принимает те же параметры, что и создание рассылки.
      parameters:
        - $ref: '#/components/parameters/ContentType'
        - $ref: '#/components/parameters/Accept'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, text, list_id]
              properties:
                name: { type: string, minLength: 1, maxLength: 40 }
                text:
                  type: string
                  minLength: 1
                  maxLength: 1600
                  description: |
                    Текст SMS. Поддерживает переменные `{{varname}}`. Стоимость рассчитывается
                    по «худшему случаю» — берётся самое длинное значение каждой переменной.
                list_id:
                  type: array
                  minItems: 1
                  maxItems: 6000
                  items:
                    oneOf:
                      - type: integer
                      - type: object
                        required: [number]
                        properties:
                          number: { type: string, minLength: 12, maxLength: 12 }
                          extra: { type: string, maxLength: 60 }
                          variables:
                            type: object
                            additionalProperties: { type: string }
                            description: Переменные контакта для подстановки в текст через `{{varname}}`.
                date_from: { type: string, minLength: 10, maxLength: 10 }
                time_from: { type: string, minLength: 8, maxLength: 8 }
                client_id: { type: string, maxLength: 60 }
                callback_url: { type: string, maxLength: 256 }
      responses:
        '201':
          description: Максимальная стоимость рассылки.
          content:
            application/json:
              schema:
                type: object
                properties:
                  max_cost: { type: number }
              example:
                max_cost: 120.00
        '401': { $ref: '#/components/responses/401' }
        '422': { $ref: '#/components/responses/422' }
        '429': { $ref: '#/components/responses/429' }
        '500': { $ref: '#/components/responses/500' }

  /messages/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema: { type: string }
        description: ID сообщения
    get:
      summary: Показать сообщение
      operationId: get-messages-id
      tags: [SMS рассылки]
      description: Возвращает детальную информацию о конкретном SMS сообщении.
      parameters:
        - $ref: '#/components/parameters/ContentType'
        - $ref: '#/components/parameters/Accept'
      responses:
        '200':
          description: Объект сообщения.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Message' }
        '401': { $ref: '#/components/responses/401' }
        '404': { $ref: '#/components/responses/404' }
        '429': { $ref: '#/components/responses/429' }

  # ─────────────────────────────────────────
  # НАСТРОЙКИ
  # ─────────────────────────────────────────

  /settings:
    get:
      summary: Получить настройки аккаунта
      operationId: get-settings
      tags: [Настройки]
      description: Возвращает текущие настройки аккаунта, тарифы, подтверждённые номера и общую статистику.
      parameters:
        - $ref: '#/components/parameters/ContentType'
        - $ref: '#/components/parameters/Accept'
      responses:
        '200':
          description: Объект с настройками аккаунта.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/AccountSettings' }
        '401': { $ref: '#/components/responses/401' }
        '429': { $ref: '#/components/responses/429' }

    put:
      summary: Редактировать настройки
      operationId: put-settings
      tags: [Настройки]
      description: Изменяет имя, подписки и визитную карточку аккаунта.
      parameters:
        - $ref: '#/components/parameters/ContentType'
        - $ref: '#/components/parameters/Accept'
      requestBody:
        description: Отправьте только те поля, которые нужно изменить.
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                  minLength: 2
                  maxLength: 40
                  description: Имя пользователя
                subscribe:
                  type: boolean
                  description: Подписка на новости через звонки
                notifications_subscribe:
                  type: boolean
                  description: Дублирование уведомлений через SMS
                visit_card_audio:
                  type: integer
                  description: ID аудиозаписи для визитной карточки
                visit_card_text:
                  type: string
                  maxLength: 160
                  description: Текст SMS визитной карточки (до 1 SMS)
            example:
              name: "Жандос"
              subscribe: true
              notifications_subscribe: false
              visit_card_text: "Компания AutoCall.kz, тел: +77470000000"
      responses:
        '200':
          description: Обновлённые настройки аккаунта.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/AccountSettings' }
        '401': { $ref: '#/components/responses/401' }
        '422': { $ref: '#/components/responses/422' }
        '429': { $ref: '#/components/responses/429' }
        '500': { $ref: '#/components/responses/500' }

  /settings/{visit_card_type}:
    parameters:
      - name: visit_card_type
        in: path
        required: true
        schema:
          type: string
          enum: [visit_card_audio, visit_card_text]
        description: Тип визитной карточки
    delete:
      summary: Удалить визитную карточку
      operationId: delete-settings-visit-card
      tags: [Настройки]
      description: Удаляет аудио или текстовую визитную карточку аккаунта.
      parameters:
        - $ref: '#/components/parameters/ContentType'
        - $ref: '#/components/parameters/Accept'
      responses:
        '204':
          description: Визитная карточка удалена.
        '401': { $ref: '#/components/responses/401' }
        '422': { $ref: '#/components/responses/422' }
        '429': { $ref: '#/components/responses/429' }
        '500': { $ref: '#/components/responses/500' }

  /settings/confirmed_numbers:
    get:
      summary: Получить подтверждённые номера
      operationId: get-settings-confirmed-numbers
      tags: [Настройки]
      description: Возвращает все номера телефонов, добавленные в аккаунт.
      parameters:
        - $ref: '#/components/parameters/ContentType'
        - $ref: '#/components/parameters/Accept'
      responses:
        '200':
          description: Массив с номерами.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/ConfirmedNumber'
              example:
                - id: 1
                  number: "+77010000001"
                  verified_at: "2019-12-08 23:46:06"
                  created_at: "2019-12-08 23:46:06"
                  updated_at: "2019-12-08 23:46:06"
                - id: 3
                  number: "+77010000002"
                  verified_at: null
                  created_at: "2024-03-19 17:10:27"
                  updated_at: "2024-03-19 17:10:27"
        '401': { $ref: '#/components/responses/401' }
        '429': { $ref: '#/components/responses/429' }

    post:
      summary: Добавить или подтвердить номер
      operationId: post-settings-confirmed-numbers
      tags: [Настройки]
      description: |
        Двухшаговый процесс:
        1. Отправьте `number` — система позвонит на этот номер, последние 5 цифр номера звонящего = код подтверждения
        2. Отправьте `verification_code` — номер будет подтверждён

        Неподтверждённые номера удаляются автоматически через 10 минут.
      parameters:
        - $ref: '#/components/parameters/ContentType'
        - $ref: '#/components/parameters/Accept'
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                number:
                  type: string
                  minLength: 12
                  maxLength: 12
                  pattern: '^\+7(700|701|702|705|707|708|747|771|775|776|777|778)\d{7}$'
                  example: "+77020000001"
                  description: Номер для добавления (шаг 1)
                verification_code:
                  type: integer
                  minimum: 10000
                  maximum: 99999
                  description: Код подтверждения из последних 5 цифр входящего звонка (шаг 2)
            examples:
              Шаг 1 — добавить номер:
                value:
                  number: "+77020000001"
              Шаг 2 — подтвердить:
                value:
                  verification_code: 30756
      responses:
        '201':
          description: Номер добавлен (шаг 1) или подтверждён (шаг 2).
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ConfirmedNumber' }
        '401': { $ref: '#/components/responses/401' }
        '422': { $ref: '#/components/responses/422' }
        '429': { $ref: '#/components/responses/429' }
        '500': { $ref: '#/components/responses/500' }

  # ─────────────────────────────────────────
  # ПЛАТЕЖИ
  # ─────────────────────────────────────────

  /balance:
    get:
      summary: Получить баланс
      operationId: get-balance
      tags: [Платежи]
      description: Возвращает текущий баланс аккаунта.
      parameters:
        - $ref: '#/components/parameters/ContentType'
        - $ref: '#/components/parameters/Accept'
      responses:
        '200':
          description: Объект с балансом.
          content:
            application/json:
              schema:
                type: object
                required: [balance, currency]
                properties:
                  balance:
                    type: number
                    description: Остаток на балансе
                  currency:
                    type: string
                    description: Символ валюты
              example:
                balance: 1450.75
                currency: "₸"
        '401': { $ref: '#/components/responses/401' }
        '429': { $ref: '#/components/responses/429' }

  /payments:
    get:
      summary: Получить транзакции
      operationId: get-payments
      tags: [Платежи]
      description: Возвращает полную информацию о балансе и историю транзакций с постраничным выводом.
      parameters:
        - $ref: '#/components/parameters/ContentType'
        - $ref: '#/components/parameters/Accept'
        - $ref: '#/components/parameters/Page'
      responses:
        '200':
          description: Объект с данными баланса и транзакциями.
          content:
            application/json:
              schema:
                type: object
                required: [balance, currency, reserved, incoming, outgoing, paid, used, transactions]
                properties:
                  balance: { type: number }
                  currency: { type: string }
                  reserved: { type: number, description: Замороженная сумма }
                  incoming: { type: number, description: Обороты по приходу }
                  outgoing: { type: number, description: Обороты по расходу }
                  paid: { type: number, description: Всего пополнено }
                  used: { type: number, description: Всего израсходовано }
                  transactions:
                    allOf:
                      - $ref: '#/components/schemas/Pagination'
                      - type: object
                        properties:
                          data:
                            type: array
                            items:
                              $ref: '#/components/schemas/Transaction'
        '401': { $ref: '#/components/responses/401' }
        '429': { $ref: '#/components/responses/429' }

    post:
      summary: Пополнить баланс
      operationId: post-payments
      tags: [Платежи]
      description: Создаёт платёжную сессию и возвращает ссылку на оплату через VISA/MASTERCARD. Минимальная сумма — 200 тенге.
      parameters:
        - $ref: '#/components/parameters/ContentType'
        - $ref: '#/components/parameters/Accept'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [amount]
              properties:
                amount:
                  type: integer
                  minimum: 200
                  description: Сумма пополнения в тенге
            example:
              amount: 1000
      responses:
        '201':
          description: Ссылка на страницу оплаты.
          content:
            application/json:
              schema:
                type: object
                required: [payment_url]
                properties:
                  payment_url:
                    type: string
                    description: URL страницы оплаты
              example:
                payment_url: "https://3ds.kassanova.kz/payment/merchants/kassanova/payment.html?mdOrder=b5f7a05f&language=ru"
        '401': { $ref: '#/components/responses/401' }
        '422': { $ref: '#/components/responses/422' }
        '429': { $ref: '#/components/responses/429' }
        '500': { $ref: '#/components/responses/500' }

  # ─────────────────────────────────────────
  # ПАНЕЛЬ МОНИТОРИНГА
  # ─────────────────────────────────────────

  /dashboard:
    get:
      summary: Получить сводку аккаунта
      operationId: get-dashboard
      tags: [Панель мониторинга]
      description: |
        Баланс, замороженные средства, активные кампании и потребление за период.

        Период задаётся параметрами `from` и `to`; по умолчанию — последние 30 дней,
        максимум — 366 дней.

        #### Что изменилось в 1.13
        Ответ переписан. Прежняя версия отдавала `week_activity` (7 дней) и `full_activity`
        (за всё время) и считала их прямым перебором таблицы звонков: у крупного аккаунта
        запрос шёл около 24 секунд. Теперь данные приходят из суточной витрины, ответ —
        единицы миллисекунд, а период задаёт клиент.

        | Было | Стало |
        |------|-------|
        | `running&awaiting` | `campaigns.active` |
        | `in_queue` | `campaigns.in_queue` |
        | `week_activity` (7 дней) | `daily` (любой период) |
        | `full_activity` (за всё время) | `totals` + `reasons` за период |
        | `success` + `recalled` раздельно | `calls_answered` одной величиной |

        ⚠️ Статистика доступна с **23 мая 2026** — за более ранние даты витрина пуста.
      parameters:
        - $ref: '#/components/parameters/ContentType'
        - $ref: '#/components/parameters/Accept'
        - name: from
          in: query
          required: false
          description: Начало периода, `YYYY-MM-DD`. По умолчанию — 29 дней назад.
          schema: { type: string, format: date }
          example: "2026-08-01"
        - name: to
          in: query
          required: false
          description: Конец периода, `YYYY-MM-DD`. По умолчанию — сегодня.
          schema: { type: string, format: date }
          example: "2026-08-22"
      responses:
        '200':
          description: Сводка аккаунта за период.
          content:
            application/json:
              schema:
                type: object
                required: [balance, currency, reserved, campaigns, period, totals, reasons, daily]
                properties:
                  balance:
                    type: number
                    description: Текущий баланс
                  currency:
                    type: string
                    description: Символ валюты
                  reserved:
                    type: number
                    description: Заморожено под активными обзвонами и рассылками; возвращается по их завершении
                  campaigns:
                    type: object
                    properties:
                      active:
                        type: integer
                        description: Обзвонов в работе (running, awaiting, moderation, generating)
                      in_queue:
                        type: integer
                        description: Номеров в очереди на звонок в этих обзвонах
                  period:
                    type: object
                    properties:
                      from: { type: string, format: date }
                      to: { type: string, format: date }
                  totals:
                    $ref: '#/components/schemas/DashboardTotals'
                  reasons:
                    $ref: '#/components/schemas/DashboardReasons'
                  daily:
                    type: array
                    description: Разбивка потребления по дням, включая дни без активности.
                    items:
                      $ref: '#/components/schemas/DashboardDay'
              example:
                balance: 158516.20
                currency: "₸"
                reserved: 0
                campaigns:
                  active: 2
                  in_queue: 1450
                period:
                  from: "2026-07-24"
                  to: "2026-08-22"
                totals:
                  calls_total: 831199
                  calls_answered: 289167
                  answer_rate: 34.8
                  call_seconds: 2953661
                  agent_seconds: 0
                  records: 0
                  sms_count: 31
                  synthesis_count: 0
                  recognition_count: 0
                  flash_count: 0
                reasons:
                  busy: 167484
                  no_answer: 342532
                  error: 26749
                  spam: 3570
                daily:
                  - date: "2026-08-21"
                    calls_total: 41230
                    calls_answered: 14884
                    call_seconds: 146112
                    agent_seconds: 0
                    records: 0
                    sms_count: 0
                    synthesis_count: 0
                    recognition_count: 0
                    flash_count: 0
        '401': { $ref: '#/components/responses/401' }
        '422': { $ref: '#/components/responses/422' }
        '429': { $ref: '#/components/responses/429' }

  # ─────────────────────────────────────────
  # УВЕДОМЛЕНИЯ
  # ─────────────────────────────────────────

  /notifications:
    get:
      summary: Получить уведомления
      operationId: get-notifications
      tags: [Уведомления]
      description: |
        Возвращает все уведомления аккаунта.

        #### Типы уведомлений
        | Тип | Описание |
        |-----|----------|
        | `SoundConfirmed` | Аудиозапись прошла/не прошла модерацию |
        | `SoundCommented` | Комментарий к аудиозаписи |
        | `SMSBulkConfirmed` | Рассылка прошла/не прошла модерацию |
        | `SMSBulkCommented` | Комментарий к рассылке |
        | `SMSBulkFinished` | Рассылка завершена |
        | `ScenarioConfirmed` | Сценарий прошёл модерацию |
        | `ScenarioCommented` | Комментарий к сценарию |
        | `AutocallConfirmed` | Обзвон прошёл/не прошёл модерацию |
        | `AutocallCommented` | Комментарий к обзвону |
        | `AutocallFinished` | Обзвон завершён |
        | `ImportComplete` | Импорт списка завершён |
        | `ImportFailure` | Ошибка импорта |
        | `NonexistentContactsDetected` | В списке обнаружены несуществующие номера |
        | `ExportIsReady` | Экспорт готов к скачиванию |
        | `ExportFailure` | Ошибка экспорта |
        | `RefreshBalance` | Изменение баланса |
        | `LowBalance` | Баланс опустился ниже установленного порога |
      parameters:
        - $ref: '#/components/parameters/ContentType'
        - $ref: '#/components/parameters/Accept'
      responses:
        '200':
          description: Объект с уведомлениями и счётчиком непрочитанных.
          content:
            application/json:
              schema:
                type: object
                required: [data, unread]
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Notification'
                  unread:
                    type: integer
                    description: Количество непрочитанных уведомлений
              example:
                data:
                  - id: "4f227359-cbc5-4b4d-811f-e8846abe6f35"
                    type: "AutocallFinished"
                    data:
                      id: 42
                      name: "Акция ноябрь"
                    read_at: null
                    created_at: "2024-11-01 10:35:00"
                    updated_at: "2024-11-01 10:35:00"
                  - id: "2c2f0bac-3e80-4504-82e5-6b41d17cfbd3"
                    type: "LowBalance"
                    data:
                      balance: 45.20
                      threshold: 100.00
                    read_at: null
                    created_at: "2024-11-01 11:00:00"
                    updated_at: "2024-11-01 11:00:00"
                unread: 2
        '401': { $ref: '#/components/responses/401' }
        '429': { $ref: '#/components/responses/429' }

  /notifications/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema: { type: string }
        description: ID уведомления (UUID)
    put:
      summary: Отметить уведомление прочитанным
      operationId: put-notifications-id
      tags: [Уведомления]
      description: Помечает уведомление как прочитанное.
      parameters:
        - $ref: '#/components/parameters/ContentType'
        - $ref: '#/components/parameters/Accept'
      responses:
        '200':
          description: ID и дата прочтения уведомления.
          content:
            application/json:
              schema:
                type: object
                required: [id, read_at]
                properties:
                  id: { type: string }
                  read_at: { type: string }
              example:
                id: "2c2f0bac-3e80-4504-82e5-6b41d17cfbd3"
                read_at: "2024-11-01 12:00:00"
        '401': { $ref: '#/components/responses/401' }
        '404': { $ref: '#/components/responses/404' }
        '429': { $ref: '#/components/responses/429' }
    delete:
      summary: Удалить уведомление
      operationId: delete-notifications-id
      tags: [Уведомления]
      description: Удаляет уведомление.
      parameters:
        - $ref: '#/components/parameters/ContentType'
        - $ref: '#/components/parameters/Accept'
      responses:
        '204':
          description: Уведомление удалено.
        '401': { $ref: '#/components/responses/401' }
        '404': { $ref: '#/components/responses/404' }
        '429': { $ref: '#/components/responses/429' }
        '500': { $ref: '#/components/responses/500' }

  # ─────────────────────────────────────────
  # CALLER IDs (аренда номеров)
  # ─────────────────────────────────────────

  /caller-ids:
    get:
      operationId: get-caller-ids
      tags: [Номерной пул]
      summary: Список арендованных номеров
      description: Возвращает список номеров, арендованных текущим пользователем, а также лимиты тарифа.
      parameters:
        - $ref: '#/components/parameters/Accept'
      responses:
        '200':
          description: Список арендованных номеров.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      type: object
                      properties:
                        id: { type: integer }
                        provider: { type: string, example: tele2 }
                        number: { type: string, example: "+77086469620" }
                        rented_until: { type: string, format: date-time }
                  max_caller_id_rentals: { type: integer, description: Максимальное количество арендованных номеров по тарифу }
                  caller_id_rent_price: { type: number, description: Стоимость аренды одного номера в тенге/мес }
        '401': { $ref: '#/components/responses/401' }

    post:
      operationId: post-caller-ids
      tags: [Номерной пул]
      summary: Арендовать номер
      description: |
        Арендует выбранный номер из пула на 1 месяц. Со счёта списывается стоимость аренды согласно тарифу.
        Перед арендой получите список доступных номеров через `GET /caller-ids/available` (веб-интерфейс)
        или выберите ID из числа свободных.

        **Лимиты:** Bronze — 0 номеров, Silver — 1, Gold — 2.
      parameters:
        - $ref: '#/components/parameters/ContentType'
        - $ref: '#/components/parameters/Accept'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [caller_id_id]
              properties:
                caller_id_id:
                  type: integer
                  description: ID записи из таблицы sip_caller_ids (свободного номера).
            example:
              caller_id_id: 42
      responses:
        '201':
          description: Номер успешно арендован.
          content:
            application/json:
              schema:
                type: object
                properties:
                  message: { type: string }
                  number: { type: string, example: "+77086469620" }
                  rented_until: { type: string, format: date-time }
        '402':
          description: Недостаточно средств на балансе.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorMessage' }
        '403':
          description: Тариф не поддерживает аренду или достигнут лимит.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorMessage' }
        '409':
          description: Выбранный номер уже занят другим пользователем.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorMessage' }
        '422': { $ref: '#/components/responses/422' }
        '401': { $ref: '#/components/responses/401' }

  /caller-ids/{id}:
    delete:
      operationId: delete-caller-ids-id
      tags: [Номерной пул]
      summary: Освободить номер досрочно
      description: Досрочно освобождает арендованный номер. **Средства не возвращаются.**
      parameters:
        - $ref: '#/components/parameters/Accept'
        - name: id
          in: path
          required: true
          schema: { type: integer }
          description: ID записи арендованного номера.
      responses:
        '200':
          description: Номер успешно освобождён.
          content:
            application/json:
              schema:
                type: object
                properties:
                  message: { type: string }
        '404':
          description: Номер не найден или не принадлежит вам.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorMessage' }
        '401': { $ref: '#/components/responses/401' }

  # ─────────────────────────────────────────
  # FLASH CALLS
  # ─────────────────────────────────────────

  /flash-calls:
    post:
      summary: Отправить Flash Call
      operationId: post-flash-calls
      tags: [Flash Calls]
      description: |
        Совершает Flash Call на указанный номер. Последние N цифр номера звонящего — это код подтверждения.
        Звонок автоматически сбрасывается, биллинг на стороне абонента не возникает.

        **Авто-код** (`digits`): сервис сам генерирует случайный код. Работает всегда.
        **Свой код** (`code`): вы передаёте нужный код. Может вернуть 422 если номер с нужными последними цифрами недоступен.

        > Рекомендуется использовать Авто-код для гарантированной доставки.

        **Rate limit:** не более 3 попыток на один номер в течение 5 минут.
      parameters:
        - $ref: '#/components/parameters/ContentType'
        - $ref: '#/components/parameters/Accept'
      requestBody:
        required: true
        description: |
          Для **Авто-кода** отправьте `number` + `digits`.
          Для **Своего кода** отправьте `number` + `code`.
        content:
          application/json:
            schema:
              type: object
              required: [number]
              properties:
                number:
                  type: string
                  minLength: 12
                  maxLength: 12
                  pattern: '^\+7(700|701|702|705|706|707|708|747|771|775|776|777|778|710|711|712|713|714|715|716|717|718|721|722|723|724|725|726|727|728|729|736)\d{7}$'
                  example: "+77000000000"
                  description: Номер получателя в формате +7XXXXXXXXXX
                digits:
                  type: integer
                  minimum: 3
                  maximum: 6
                  description: |
                    **Авто-код**: количество цифр кода (3–6).
                    Обязательно если `code` не передан.
                code:
                  type: string
                  pattern: '^\d{3,6}$'
                  minLength: 3
                  maxLength: 6
                  example: "1234"
                  description: |
                    **Свой код**: числовой код от 3 до 6 цифр.
                    Обязательно если `digits` не передан.
            examples:
              Авто-код из 4 цифр:
                value:
                  number: "+77010000000"
                  digits: 4
              Свой код:
                value:
                  number: "+77010000000"
                  code: "7823"
      responses:
        '201':
          description: Flash Call отправлен. Код подтверждения — последние N цифр поля `code`.
          content:
            application/json:
              schema:
                type: object
                required: [id, code, number]
                properties:
                  id:
                    type: integer
                    description: ID Flash Call записи
                  code:
                    type: string
                    description: Код подтверждения (последние N цифр номера звонящего)
                  number:
                    type: string
                    description: Номер получателя
              example:
                id: 1023
                code: "7482"
                number: "+77010000000"
        '401': { $ref: '#/components/responses/401' }
        '402':
          description: Недостаточно средств на балансе.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorMessage' }
        '422':
          description: |
            Ошибка валидации: неверно заполнены поля, либо (для режима Свой код) номер с нужными цифрами недоступен.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ValidationError' }
        '429':
          description: Превышен rate limit — не более 3 попыток на один номер в течение 5 минут.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorMessage' }

# ─────────────────────────────────────────
# КОМПОНЕНТЫ
# ─────────────────────────────────────────

  /tariffs:
    servers:
      - url: https://autocall.kz/api/public
    get:
      operationId: getTariffs
      summary: Получить актуальные тарифы
      tags: [Публичный API]
      security: []
      description: Возвращает список тарифных планов с ценами на звонки, SMS и синтез речи. Данные кешируются на 1 час.
      responses:
        "200":
          description: Список тарифов
          content:
            application/json:
              schema:
                type: object
                properties:
                  currency:
                    type: string
                    example: KZT
                  note:
                    type: string
                  tariffs:
                    type: array
                    items:
                      $ref: "#/components/schemas/Tariff"
              example:
                currency: KZT
                note: "Цены за единицу: звонок (секунда тарификации), SMS (1 сообщение)."
                tariffs:
                  - name: BRONZE
                    threshold: 0
                    call_price: 3.5
                    sms_price: 18
                    synthesis_price: 2
                    speech_price: 2
                    recording_price: 1
                  - name: SILVER
                    threshold: 50000
                    call_price: 2.8
                    sms_price: 16
                    synthesis_price: 1.8
                    speech_price: 1.8
                    recording_price: 0.8
                  - name: GOLD
                    threshold: 200000
                    call_price: 2.2
                    sms_price: 14
                    synthesis_price: 1.5
                    speech_price: 1.5
                    recording_price: 0.5

  /test-call:
    servers:
      - url: https://autocall.kz/api/public
    post:
      operationId: sendTestCall
      summary: Отправить тестовый звонок
      tags: [Публичный API]
      security: []
      description: |
        Инициирует реальный тестовый звонок от AutoCall.kz на указанный казахстанский номер телефона.

        Ограничения:
        - Один звонок на номер в сутки
        - Доступно с 09:00 до 21:30 по Алматы (UTC+6)
        - Только казахстанские мобильные номера (7xxxxxxxxxx)
        - Rate limit: 2 запроса в минуту с одного IP
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - phone
              properties:
                phone:
                  type: string
                  description: Казахстанский мобильный номер без +7, только цифры после 7
                  pattern: "^(700|701|702|705|706|707|708|747|771|775|776|777|778|710|711|712|713|714|715|716|717|718|721|722|723|724|725|726|727|728|729|736)\\d{7}$"
                  example: "7771234567"
            example:
              phone: "7771234567"
      responses:
        "200":
          description: Звонок успешно инициирован
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                  number:
                    type: string
              example:
                message: "Тестовый звонок отправлен на номер +77000000000. Ожидайте звонка в течение 1–2 минут."
                number: "+77000000000"
        "422":
          description: Валидация не прошла или звонки недоступны сейчас
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorMessage"
        "429":
          description: Номер уже получил тестовый звонок сегодня или превышен rate limit
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorMessage"
        "403":
          description: Номер в чёрном списке или запрос заблокирован
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorMessage"

  # ─────────────────────────────────────────
  # СПИСКИ КОНТАКТОВ
  # ─────────────────────────────────────────

components:

  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: 'Заголовок: Authorization: Bearer ВАШ_API_КЛЮЧ'

  parameters:
    ContentType:
      name: Content-Type
      in: header
      required: true
      schema:
        type: string
        default: application/json
      description: Тип содержимого запроса
    Accept:
      name: Accept
      in: header
      required: true
      schema:
        type: string
        default: application/json
      description: Тип содержимого ответа
    DateFrom:
      name: date_from
      in: query
      required: false
      schema:
        type: string
        example: "2024-01-01"
      description: "Фильтр: с какой даты (формат Y-m-d)"
    DateTo:
      name: date_to
      in: query
      required: false
      schema:
        type: string
        example: "2024-12-31"
      description: "Фильтр: по какую дату (формат Y-m-d)"
    Order:
      name: order
      in: query
      required: false
      schema:
        type: string
        enum: [asc, desc]
      description: Сортировка по дате создания
    ClientIdFilter:
      name: client_id
      in: query
      required: false
      schema:
        type: string
      description: Фильтр по client_id из Вашей системы
    Page:
      name: page
      in: query
      required: false
      schema:
        type: integer
        minimum: 1
      description: Номер страницы

  responses:
    '401':
      description: Не передан или недействителен API ключ.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorMessage' }
          example:
            message: "Unauthenticated."
    '404':
      description: Ресурс не найден по указанному ID.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorMessage' }
          example:
            message: "Not found."
    '422':
      description: Ошибка валидации — данные запроса не прошли проверку.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ValidationError' }
          example:
            message: "The given data was invalid."
            errors:
              name: ["Поле name обязательно."]
    '429':
      description: |
        Превышен лимит запросов — либо 300 в минуту на ключ, либо 15 в секунду на IP-адрес.
        Тело показано для лимита приложения; серверный лимит отдаёт HTML.
        Повторяйте запрос с нарастающей паузой, а не сразу.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorMessage' }
          example:
            message: "Too Many Attempts."
    '500':
      description: Внутренняя ошибка сервера.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorMessage' }
          example:
            message: "Server Error."

  schemas:

    ErrorMessage:
      type: object
      required: [message]
      properties:
        message:
          type: string
          description: Описание ошибки

    ValidationError:
      type: object
      required: [message, errors]
      properties:
        message:
          type: string
          description: Общее описание ошибки
        errors:
          type: object
          description: Объект с ошибками по каждому полю
          additionalProperties:
            type: array
            items:
              type: string

    Tariff:
      type: object
      properties:
        name:
          type: string
          enum: [BRONZE, SILVER, GOLD]
          description: Название тарифного плана
        threshold:
          type: integer
          description: Минимальный баланс для активации тарифа (тенге)
        call_price:
          type: number
          description: Цена за секунду исходящего звонка (тенге)
        sms_price:
          type: number
          description: Цена за одно SMS-сообщение (тенге)
        synthesis_price:
          type: number
          description: Цена за секунду синтеза речи (тенге)
        speech_price:
          type: number
          description: Цена за секунду распознавания речи (тенге)
        recording_price:
          type: number
          description: Цена за секунду записи звонка (тенге)

    Pagination:
      type: object
      description: Стандартный объект постраничного вывода Laravel
      required: [current_page, data, first_page_url, from, last_page, last_page_url, next_page_url, path, per_page, prev_page_url, to, total]
      properties:
        current_page: { type: integer }
        data:
          type: array
          items: {}
        first_page_url: { type: string }
        from: { type: integer }
        last_page: { type: integer }
        last_page_url: { type: string }
        next_page_url: { type: [string, "null"] }
        path: { type: string }
        per_page: { type: integer }
        prev_page_url: { type: [string, "null"] }
        to: { type: integer }
        total: { type: integer }

    DashboardTotals:
      type: object
      description: Потребление за период. Источник — суточная витрина, те же цифры, что в личном кабинете.
      properties:
        calls_total: { type: integer, description: Набрано номеров (без тестовых обзвонов) }
        calls_answered: { type: integer, description: "Состоявшихся контактов: взяли трубку либо перезвонили сами" }
        answer_rate:
          type: [number, "null"]
          description: Доля дозвона в процентах. `null`, если звонков не было — ноль читался бы как «звонили и не дозвонились».
        call_seconds: { type: integer, description: Секунд разговора, включая перезвоны абонентов }
        agent_seconds: { type: integer, description: Из них секунд разговора с ИИ-агентом }
        records: { type: integer, description: Записей разговоров }
        sms_count:
          type: integer
          description: SMS в тарифицируемых СЕГМЕНТАХ — длинное сообщение режется на несколько и столько же раз оплачивается.
        synthesis_count: { type: integer, description: Синтезов речи }
        recognition_count: { type: integer, description: Распознаваний речи }
        flash_count: { type: integer, description: Flash Call }

    DashboardReasons:
      type: object
      description: Почему не дозвонились. Сумма причин равна `calls_total − calls_answered`.
      properties:
        busy: { type: integer, description: Занято }
        no_answer: { type: integer, description: Не отвечает }
        error: { type: integer, description: Номер недоступен или не существует }
        spam: { type: integer, description: Абонент пожаловался на спам }

    DashboardDay:
      type: object
      description: Потребление за один день. Дни без активности присутствуют с нулями.
      properties:
        date: { type: string, format: date }
        calls_total: { type: integer }
        calls_answered: { type: integer }
        call_seconds: { type: integer }
        agent_seconds: { type: integer }
        records: { type: integer }
        sms_count: { type: integer }
        synthesis_count: { type: integer }
        recognition_count: { type: integer }
        flash_count: { type: integer }

    Contact:
      type: object
      required: [id, number, extra, variables, created_at, updated_at]
      properties:
        id: { type: integer }
        list_id: { type: integer }
        number: { type: string }
        extra: { type: [string, "null"], description: Дополнительное описание (ФИО и т.п.) }
        variables:
          type: [object, "null"]
          description: |
            Переменные контакта для персонализации SMS. Используются в шаблонах через `{{varname}}`.
            Ключи — только латиница, цифры и `_` (до 30 символов). Значения — строки до 255 символов.
          example: { name: "Иванов Иван", debt: "15000", contract: "№123" }
          additionalProperties: { type: string }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }

    ContactList:
      type: object
      required: [id, client_id, name, importing, nonexistent, created_at, updated_at, contacts_count, used]
      properties:
        id: { type: integer }
        client_id: { type: [string, "null"] }
        name: { type: string }
        importing: { type: boolean, description: Идёт ли импорт прямо сейчас }
        nonexistent:
          type: [array, "null"]
          description: Массив ID контактов с несуществующими номерами
          items: { type: integer }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
        contacts_count: { type: integer }
        used: { type: integer, description: Сколько раз список использовался }

    Sound:
      type: object
      required: [id, client_id, name, duration, confirmed, comment, confirmed_at, created_at, updated_at, playback_url, used]
      properties:
        id: { type: integer }
        client_id: { type: [string, "null"] }
        name: { type: string }
        duration: { type: integer, description: Длительность в секундах }
        confirmed: { type: boolean, description: Одобрена ли модераторами }
        comment: { type: [string, "null"], description: Комментарий модератора }
        confirmed_at: { type: [string, "null"] }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
        playback_url: { type: string, description: URL для прослушивания/скачивания }
        used: { type: integer, description: Количество использований в обзвонах }

    Scenario:
      type: object
      required: [id, client_id, name, max_cost, comment, created_at, updated_at, used]
      properties:
        id: { type: integer }
        client_id: { type: [string, "null"] }
        name: { type: string }
        max_cost: { type: [string, number], description: Максимальная стоимость на 1 звонок }
        comment: { type: [string, "null"] }
        confirmed_at: { type: [string, "null"] }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
        used: { type: integer }

    ScenarioDetail:
      allOf:
        - $ref: '#/components/schemas/Scenario'
        - type: object
          properties:
            data:
              type: object
              description: |
                Граф сценария — здесь **объектом**, а не строкой (в отличие от `data` при
                создании/редактировании). Ключи = id элементов. Структура — в описании раздела.

    Voice:
      type: object
      properties:
        name: { type: string, description: "Значение для поля `voice` при создании агента", example: Sulafat }
        display: { type: string, description: Как голос называется в личном кабинете, example: Асель }
        gender: { type: string, enum: [male, female] }
        characteristic: { type: [string, "null"], description: Краткое описание тембра/манеры }
        sample: { type: string, description: Ссылка на аудиофрагмент голоса }
        avatar: { type: string, description: Ссылка на аватар голоса }
        has_avatar: { type: boolean }

    AgentDataField:
      type: object
      required: [key]
      properties:
        key:
          type: string
          maxLength: 40
          description: |
            Идентификатор поля — только латиница, цифры и `_`. Под этим именем значение
            попадёт в результат звонка.
          example: confirmed
        label:
          type: string
          maxLength: 100
          description: Человеческое название колонки в отчёте. Не передали — возьмём `key`.
          example: Подтвердил приём

    AgentTool:
      type: object
      required: [type, name]
      description: |
        Действие, которое агент может выполнить прямо во время разговора.
        Решение о вызове принимает сам агент — по полю `when`.
      properties:
        type:
          type: string
          enum: [sms, http]
          description: "`sms` — отправить сообщение абоненту, `http` — запрос на ваш сервер."
        name:
          type: string
          maxLength: 30
          description: Короткое имя действия — латиница, цифры и `_`.
          example: price
        when:
          type: string
          maxLength: 200
          description: В какой момент разговора это нужно сделать.
          example: клиент попросил прислать прайс
        text:
          type: string
          maxLength: 500
          description: |
            Только для `type=sms`. Текст сообщения — уходит абоненту буквально, агент его
            не переписывает. Оплачивается по вашему тарифу SMS.
        method:
          type: string
          enum: [GET, POST, PUT, PATCH, DELETE]
          default: GET
          description: Только для `type=http`.
        url:
          type: string
          maxLength: 500
          description: |
            Только для `type=http`. Адрес вашего сервера. Поддерживает подстановки
            `{{phone}}`, `{{name}}` и другие переменные контакта.

            Запросы во внутренние и приватные сети запрещены — только публичные адреса.
        headers:
          type: array
          maxItems: 10
          description: Только для `type=http`. Заголовки запроса, в т.ч. авторизация.
          items:
            type: object
            required: [key]
            properties:
              key: { type: string, maxLength: 100 }
              value: { type: string, maxLength: 500 }
        bodyType:
          type: string
          enum: [none, json]
          default: none
          description: Только для `type=http`.
        body:
          type: string
          maxLength: 2000
          description: Только для `type=http`. Тело запроса при `bodyType=json`. Поддерживает `{{переменные}}`.
        response_template:
          type: string
          maxLength: 500
          description: |
            Только для `type=http`. Фраза, которую агент произнесёт вслух, с подстановкой
            полей из ответа вашего сервера — например `Ваш заказ {{status}}`. Сырой JSON
            абоненту никогда не зачитывается.
        fallback:
          type: string
          maxLength: 300
          description: Только для `type=http`. Что сказать, если запрос не удался.

    Agent:
      type: object
      properties:
        id: { type: integer }
        client_id: { type: [string, "null"] }
        name: { type: string }
        voice: { type: string, description: Технический идентификатор голоса }
        voice_name: { type: string, description: Название голоса в личном кабинете }
        gender: { type: string, enum: [male, female] }
        prompt: { type: [string, "null"] }
        data_schema:
          type: array
          items: { $ref: '#/components/schemas/AgentDataField' }
        tools:
          type: array
          items: { $ref: '#/components/schemas/AgentTool' }
        max_seconds: { type: integer }
        status:
          type: string
          enum: [draft, generating, ready, failed]
        active: { type: boolean }
        used:
          type: integer
          description: В скольких обзвонах агент уже задействован.
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }

    AgentCreate:
      type: object
      required: [name, voice, prompt, data_schema]
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 60
          description: Имя агента — он представляется им в разговоре.
        voice:
          type: string
          description: Идентификатор голоса из `GET /agents/voices`.
        prompt:
          type: string
          minLength: 1
          maxLength: 10000
          description: |
            Сценарий разговора обычным текстом.

            **Обязательно** содержит три непустые секции — `## Кто ты`, `## Задача звонка`,
            `## Ход разговора`. Без любой из них — `422`. Подробности и пример — в описании
            `POST /agents`.

            Правила безопасности дописываются нами автоматически — включать их в текст не нужно.
        data_schema:
          type: array
          minItems: 1
          maxItems: 30
          description: Поля, которые агент должен вынести из разговора.
          items: { $ref: '#/components/schemas/AgentDataField' }
        tools:
          type: array
          maxItems: 20
          items: { $ref: '#/components/schemas/AgentTool' }
        max_seconds:
          type: integer
          default: 300
          minimum: 120
          maximum: 600
          description: Потолок длительности разговора в секундах. Он же потолок стоимости звонка.
        client_id:
          type: string
          maxLength: 60
          description: Ваш идентификатор агента — по нему агента можно найти через `GET /agents?client_id=`.

    AgentUpdate:
      type: object
      description: Все поля необязательны — меняются только присланные.
      properties:
        name: { type: string, minLength: 1, maxLength: 60 }
        voice: { type: string }
        prompt:
          type: string
          minLength: 1
          maxLength: 10000
          description: |
            Присылаете — присылайте сценарий целиком. Требование к секциям то же, что при
            создании: `## Кто ты`, `## Задача звонка`, `## Ход разговора` должны быть на месте
            и непустыми.
        data_schema:
          type: array
          minItems: 1
          maxItems: 30
          items: { $ref: '#/components/schemas/AgentDataField' }
        tools:
          type: array
          maxItems: 20
          items: { $ref: '#/components/schemas/AgentTool' }
        max_seconds: { type: integer, minimum: 120, maximum: 600 }
        active: { type: boolean, description: "`false` — временно вывести агента из оборота." }
        client_id: { type: string, maxLength: 60 }

    AutocallSummary:
      type: object
      properties:
        id: { type: integer }
        client_id: { type: [string, "null"] }
        name: { type: string }
        date_from: { type: string }
        time_from: { type: string }
        time_to: { type: string }
        max_tries: { type: integer }
        tries_interval: { type: integer }
        type:
          type: string
          enum: [regular, interactive, agent]
        status:
          type: string
          enum: [running, completed, awaiting, moderation, denied]
        max_cost: { type: [number, string] }
        final_cost: { type: [number, string, "null"] }
        topup_cost:
          type: [number, string, "null"]
          description: >-
            Досписание за абонентов, которые перезвонили сами уже после завершения обзвона.
            Они проходят сценарий целиком, поэтому распознавание, синтез, SMS, разговор с
            ИИ-агентом и запись оплачиваются отдельной операцией; связь за входящий звонок
            не тарифицируется. Значение уже входит в final_cost. null — добора не было.
        topped_up_at:
          type: [string, "null"]
          format: date-time
          description: Когда отработал досчёт за перезвонивших. null — ещё не отработал.
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }

    AutocallCreate:
      type: object
      required: [name, type, list_id]
      properties:
        name: { type: string, minLength: 1, maxLength: 40 }
        time_from: { type: string, default: "12:00:00", minLength: 8, maxLength: 8 }
        time_to: { type: string, default: "18:00:00", minLength: 8, maxLength: 8 }
        type:
          type: string
          enum: [regular, interactive, agent]
          default: regular
        audio_id:
          type: array
          description: "Обязательно если type=regular. ID аудиозаписей или объекты для создания новых."
          items:
            oneOf:
              - type: integer
              - type: object
                required: [type, data]
                properties:
                  type: { type: string, enum: [synthesis, upload] }
                  data:
                    type: object
                    properties:
                      text: { type: string, minLength: 1, maxLength: 400, description: "Текст для синтеза. У голосов с emotions=true поддерживает подсказки интонации в квадратных скобках — см. POST /sounds." }
                      voice:
                        type: string
                        default: Sulafat
                        description: Идентификатор голоса из `GET /sounds/voices`. По умолчанию `Sulafat`.
                      speed: { type: number, default: 1, minimum: 0.25, maximum: 4 }
                      pitch: { type: number, default: 0, minimum: -20, maximum: 20, description: "Только для голосов с pitch=true в GET /sounds/voices." }
                      file: { type: string, description: Base64 строка файла mp3/wav/ogg }
        scenario_id: { type: integer, description: "Обязательно если type=interactive." }
        agent_id:
          type: integer
          description: |
            Обязательно если `type=agent`. ID вашего ИИ-агента со статусом `ready`
            (см. `GET /agents`). Агент в статусе `generating`, отключённый (`active=false`)
            или удалённый вернёт `422`.
        list_id:
          type: array
          minItems: 1
          maxItems: 5000
          items:
            oneOf:
              - type: integer
              - type: object
                required: [number]
                properties:
                  number: { type: string, minLength: 12, maxLength: 12 }
                  extra: { type: string, maxLength: 255 }
                  variables:
                    type: object
                    description: |
                      Переменные контакта для подстановки в SMS-тексты (`text`, `text_unanswered`)
                      через `{{varname}}`. Ключи — `[a-zA-Z0-9_]` (до 30 символов),
                      значения — строки (до 255 символов).
                    additionalProperties: { type: string }
                    example: { name: "Иван", debt: "15000" }
        max_tries: { type: integer, default: 1, enum: [1, 2, 3, 4] }
        tries_interval: { type: integer, default: 60, enum: [15, 30, 60, 120] }
        date_from: { type: string, minLength: 10, maxLength: 10 }
        sms: { type: boolean, default: false, description: "Отправить SMS при успешном дозвоне?" }
        text:
          type: string
          description: |
            Текст SMS при успешном дозвоне (обязательно если `sms=true`).
            Поддерживает переменные контакта `{{varname}}` — например `Здравствуйте, {{name}}!`.
        sms_unanswered: { type: boolean, default: false, description: "Отправить SMS если не дозвонились?" }
        text_unanswered:
          type: string
          description: |
            Текст SMS для недозвонившихся (обязательно если `sms_unanswered=true`).
            Поддерживает переменные контакта `{{varname}}`.
        lifetime: { type: boolean, default: false, deprecated: true, description: "Не используется с 1.13.3. Поле принимается и сохраняется ради совместимости, но на поведение не влияет: обзвон всегда завершается сразу, а услуги, оказанные перезвонившим абонентам, досчитываются отдельным списанием после закрытия окна перезвонов." }
        record: { type: boolean, default: false, description: "Записывать разговоры?" }
        client_id: { type: string, maxLength: 60 }
        callback_url: { type: string, maxLength: 256 }
        caller_id:
          type: string
          nullable: true
          maxLength: 20
          description: |
            Номер для исходящих звонков. Если не указан или `null` — выбирается случайный номер из пула.
            Можно передать конкретный арендованный вами номер (например `+77086469620`).
            Номер должен быть арендован вами и не истёкшим. Получить список арендованных номеров: `GET /caller-ids`.

    AutocallDetail:
      allOf:
        - $ref: '#/components/schemas/AutocallSummary'
        - type: object
          properties:
            sms: { type: boolean }
            sms_unanswered: { type: boolean }
            text: { type: [string, "null"] }
            text_unanswered: { type: [string, "null"] }
            audio_id: { type: [array, "null"], items: { type: [number, string] } }
            scenario_id: { type: [integer, "null"] }
            agent_id: { type: [integer, "null"], description: ИИ-агент обзвона (только для type=agent) }
            processed: { type: integer }
            speeches: { type: integer }
            lifetime: { type: boolean }
            record: { type: boolean }
            comment: { type: [string, "null"] }
            callback_url: { type: [string, "null"] }
            caller_id: { type: [string, "null"], description: Арендованный номер для звонков (null = случайный из пула) }
            finished_at: { type: [string, "null"] }
            calls_count: { type: integer }
            processed_percentage: { type: number }
            counters:
              type: object
              properties:
                success_percentage: { type: number }
                success: { type: integer }
                recalled: { type: integer }
                no_answer: { type: integer }
                busy: { type: integer }
                error: { type: integer }
                spam: { type: integer }
                tries: { type: integer }
                duration: { type: integer, description: Всего секунд разговора (биллинг) }
                total: { type: integer }
            calls:
              allOf:
                - $ref: '#/components/schemas/Pagination'
                - type: object
                  properties:
                    data:
                      type: array
                      items: { $ref: '#/components/schemas/Call' }

    Call:
      type: object
      required: [id, autocall_id, number, extra, lock, tries, date_tries, status_tries, last_try_at, duration, r_duration, status, created_at, updated_at]
      properties:
        id: { type: integer }
        autocall_id: { type: integer }
        number: { type: string }
        extra: { type: [string, "null"] }
        lock: { type: boolean, description: Идёт ли звонок прямо сейчас }
        tries: { type: integer }
        date_tries: { type: [array, "null"], items: { type: string } }
        status_tries: { type: [array, "null"], items: { type: string } }
        last_try_at: { type: [string, "null"] }
        duration: { type: integer, description: Длительность исходящего разговора в секундах (биллинг) }
        r_duration: { type: integer, description: Длительность входящего разговора (бесплатно) }
        status:
          type: string
          enum: [in_queue, success, recalled, no_answer, busy, error, spam]
        data:
          type: [object, "null"]
          description: |
            Результаты прохождения сценария и поля, извлечённые ИИ-агентом.

            Ключи, начинающиеся с подчёркивания, — служебные и не являются частью
            публичного контракта: они могут измениться без предупреждения. В частности,
            расшифровку разговора берите ручкой `GET /calls/{id}/transcript`, а не из
            `data._transcript`.
        variables:
          type: [object, "null"]
          description: Переменные контакта, скопированные при создании обзвона. Используются для подстановки в SMS текст.
          additionalProperties: { type: string }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }

    BulkSummary:
      type: object
      properties:
        id: { type: integer }
        client_id: { type: [string, "null"] }
        name: { type: string }
        status:
          type: string
          enum: [running, completed, awaiting, moderation, denied]
        date_from: { type: string }
        time_from: { type: string }
        text: { type: string }
        segments: { type: integer, description: Количество SMS сегментов }
        recipients: { type: integer }
        list_id: { type: array, items: { type: integer } }
        max_cost: { type: [string, number] }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }

    BulkDetail:
      allOf:
        - $ref: '#/components/schemas/BulkSummary'
        - type: object
          properties:
            final_cost: { type: [string, number, "null"] }
            comment: { type: [string, "null"] }
            callback_url: { type: [string, "null"] }
            messages:
              allOf:
                - $ref: '#/components/schemas/Pagination'
                - type: object
                  properties:
                    data:
                      type: array
                      items: { $ref: '#/components/schemas/Message' }

    Message:
      type: object
      required: [id, bulk_id, number, extra, variables, status, provider, init_time, status_time, created_at, updated_at]
      properties:
        id: { type: integer }
        bulk_id: { type: integer }
        number: { type: string }
        extra: { type: [string, "null"] }
        variables:
          type: [object, "null"]
          description: Переменные контакта, скопированные на момент создания рассылки. Используются для подстановки в текст.
          additionalProperties: { type: string }
        status:
          type: string
          enum: [queued, disabled, new, sent, delivered, pending, failed, expired, undeliverable, rejected]
          description: |
            | Статус | Значение | Окончательный |
            |--------|----------|---------------|
            | `queued` | в очереди | нет |
            | `new` | создано | нет |
            | `sent` | отправлено | нет |
            | `pending` | ждём отчёта | нет |
            | `delivered` | доставлено | да |
            | `failed` | ошибка | да |
            | `expired` | истекло время ожидания | да |
            | `undeliverable` | не доставлено | да |
            | `rejected` | отклонено оператором | да |
            | `disabled` | удалено | да |
        provider: { type: string, description: Мобильный оператор получателя }
        init_time: { type: [string, "null"], description: Дата отправки }
        status_time: { type: [string, "null"], description: Дата последнего изменения статуса }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }

    ConfirmedNumber:
      type: object
      required: [id, number, verified_at, created_at, updated_at]
      properties:
        id: { type: integer }
        number: { type: string, description: Номер в формате +7XXXXXXXXXX }
        verified_at:
          type: [string, "null"]
          description: Дата подтверждения. Неподтверждённые удаляются через 10 минут.
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }

    AccountSettings:
      type: object
      properties:
        name: { type: string }
        phone: { type: string, description: Номер телефона без +7 }
        sms_verified_at: { type: string }
        moderation: { type: boolean, description: Включена ли модерация }
        time_from: { type: string, description: С какого времени разрешены рассылки/обзвоны }
        time_to: { type: string, description: До какого времени разрешены рассылки/обзвоны }
        price: { type: string, description: Тариф — стоимость за секунду обзвона (₸) }
        sms_price: { type: string, description: Тариф — стоимость одного SMS (₸) }
        sip_price: { type: string, description: Тариф — стоимость за секунду через АТС (₸) }
        speech_price: { type: string, description: Тариф — стоимость распознавания речи за сеанс (₸) }
        synthesis_price: { type: string, description: Тариф — стоимость синтеза речи за сеанс (₸) }
        recording_price: { type: string, description: Тариф — стоимость записи разговора (₸) }
        recalled: { type: integer, description: Часов отслеживания перезвонивших абонентов }
        subscribe: { type: boolean, description: Подписка на новости через звонки }
        notifications_subscribe: { type: boolean, description: Дублирование уведомлений через SMS }
        visit_card_audio: { type: [integer, "null"] }
        visit_card_text: { type: [string, "null"] }
        confirmed_numbers:
          type: array
          items: { $ref: '#/components/schemas/ConfirmedNumber' }
        statistics:
          type: object
          properties:
            duration: { type: integer, description: Всего секунд разговоров }
            calls: { type: [number, string], description: Всего звонков }
            confirmed_numbers: { type: integer }
            spam_reports: { type: integer }
            lists: { type: integer }
            contacts: { type: integer }
            autocalls: { type: integer }
            bulks: { type: integer }
            sms: { type: integer }

    Transaction:
      type: object
      properties:
        id: { type: integer }
        type:
          type: string
          enum: [withdraw, deposit]
          description: withdraw — расход, deposit — приход
        confirmed: { type: boolean }
        meta:
          type: object
          properties:
            description: { type: string }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
        total:
          type: number
          description: Сумма (отрицательная для withdraw, положительная для deposit)

    Notification:
      type: object
      required: [id, type, read_at, created_at, updated_at]
      properties:
        id: { type: string, format: uuid }
        type:
          type: string
          enum:
            - SoundConfirmed
            - SoundCommented
            - SMSBulkConfirmed
            - SMSBulkCommented
            - SMSBulkFinished
            - ScenarioConfirmed
            - ScenarioCommented
            - AutocallConfirmed
            - AutocallCommented
            - AutocallFinished
            - ImportComplete
            - ImportFailure
            - NonexistentContactsDetected
            - ExportIsReady
            - ExportFailure
            - RefreshBalance
            - LowBalance
            - CallerIdExpiringSoon
            - CallerIdReleased
        data:
          type: object
          description: Содержимое уведомления (структура зависит от типа)
          properties:
            id: { type: integer }
            name: { type: string }
            type:
              type: string
              enum: [autocall, bulk]
            action:
              type: string
              enum: [deposit, withdraw]
            amount: { type: string }
            balance: { type: number, description: Текущий баланс (для LowBalance) }
            threshold: { type: number, description: Порог баланса (для LowBalance) }
        read_at: { type: [string, "null"], description: Дата прочтения (null если не прочитано) }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
