{
  "openapi": "3.1.0",
  "info": {
    "title": "YouSelfBot API",
    "version": "1.3.0",
    "summary": "Повний HTTP API платформи: збірка агента (хід, тригери, знання, дії), розмова й ескалація на живого оператора, віджет, акаунт і тарифікація. Плюс вебхуки — вхідний тригер агента і вихідні події передачі людині.",
    "description": "HTTP API платформи YouSelfBot: збирати агентів, будити їх, навчати й вбудовувати у власний застосунок, чат або CRM.\n\n## Що тут є\n\nYouSelfBot — конструктор агентів. Агент прокидається від **тригера** (повідомлення, розклад, вебхук, полінг, ручний запуск), проходить свій **пайплайн** і завершується **ходом** (Run) з обліком у кредитах.\n\nДокумент описує **весь** API, а не лише інтеграційні виклики. Кабінет — просто перший клієнт цього самого API, тож усе, що вміє він, доступно й вашому коду:\n\n| Поверхня | Що це | Чим автентифікується |\n| --- | --- | --- |\n| `/v1/api/*` | Розмова з агентом, передача людині, база знань, операторське API. | секретний ключ `sk_…` |\n| `POST /v1/hooks/{tid}` | **Вхідний вебхук**: чужа система будить агента. | HMAC-підпис тіла |\n| `/v1/widget/*` | Виклики з браузера відвідувача: розмова, вкладення, оцінки, контакти. | публічний ключ `pk_…` |\n| `/v1/dashboard/*` | Конструювання агента: хід, тригери, знання, дії, звернення, команда. | сесія користувача |\n| `/v1/auth/*`, `/v1/billing/*` | Акаунт, сесії, тарифи, кредити. | сесія користувача |\n| Вихідні вебхуки | Ми стукаємо на **вашу** адресу, коли розмову передано людині, коли відвідувач написав і коли звернення закрито. | ваш секрет підпису |\n\nПоверхні різняться не «публічністю», а тим, чим вони автентифікуються й для кого призначені. `/v1/dashboard/*` розрахований на сесію в браузері, тому зі свого бекенду зручніше жити на `/v1/api/*` і вхідних вебхуках — вони стабільніші й не потребують куки.\n\n## Автентифікація\n\nІнтеграційні виклики — заголовок `X-Bot-Key` із **секретним** ключем агента (`sk_…`) з кабінету. Кабінетні поверхні автентифікуються сесійною кукою.\n\n| Ключ | Де береться | Де працює |\n| --- | --- | --- |\n| `sk_…` (секретний) | кабінет → агент → Підключення → Ключі | `/v1/api/*`, `/v1/admin/*` |\n| `pk_…` (публічний) | сніпет віджета | лише `/v1/widget/*` у браузері |\n| сесія | `POST /v1/auth/login` | `/v1/dashboard/*`, `/v1/auth/*`, `/v1/billing/*` |\n\nПублічний ключ `pk_…` зі сніпета віджета для цього API **не підходить**: будь-який виклик `/v1/api/*` з `pk_`-ключем повертає **403** (`wrong_key_type`) — не 401. Ключ дійсний, але не того типу. Це найчастіша помилка на старті інтеграції.\n\nСекретний ключ ніколи не має потрапляти у браузер, мобільний застосунок чи публічний репозиторій — викликайте це API лише зі свого бекенду. CORS для `/v1/api/*` не налаштований саме тому.\n\n## Hello world (curl)\n\n```bash\nKEY=\"sk_ваш_секретний_ключ\"\nSESSION=$(uuidgen)   # непередбачуваний id розмови, один на діалог\n\n# 1. Перша репліка\ncurl -sS -X POST \"https://api.youselfbot.com/v1/api/chat\" \\\n  -H \"X-Bot-Key: $KEY\" -H \"Content-Type: application/json\" \\\n  -d \"{\\\"session_id\\\":\\\"$SESSION\\\",\\\"message\\\":\\\"Які у вас години роботи?\\\"}\"\n# {\"answer\":\"Ми працюємо з 9:00 до 18:00, пн–пт.\",\"session_id\":\"…\"}\n\n# 2. Наступна репліка ТОГО САМОГО діалогу — той самий session_id (так бот памʼятає контекст)\ncurl -sS -X POST \"https://api.youselfbot.com/v1/api/chat\" \\\n  -H \"X-Bot-Key: $KEY\" -H \"Content-Type: application/json\" \\\n  -d \"{\\\"session_id\\\":\\\"$SESSION\\\",\\\"message\\\":\\\"А в суботу?\\\"}\"\n\n# 3. Клієнт просить живого оператора\ncurl -sS -X POST \"https://api.youselfbot.com/v1/api/handoff\" \\\n  -H \"X-Bot-Key: $KEY\" -H \"Content-Type: application/json\" \\\n  -d \"{\\\"session_id\\\":\\\"$SESSION\\\"}\"\n# {\"status\":\"waiting\"}\n\n# 4. Опитуйте відповіді оператора. after — це поле `at` останнього ОТРИМАНОГО\n#    повідомлення (Unix ms); на першому запиті after=0.\ncurl -sS \"https://api.youselfbot.com/v1/api/messages?session_id=$SESSION&after=0\" -H \"X-Bot-Key: $KEY\"\n# {\"messages\":[{\"id\":\"…\",\"author\":\"operator\",\"text\":\"Вітаю!\",\"at\":1753600000123}],\n#  \"status\":\"operator\",\"operator_typing\":false}\n```\n\n## Ідентифікатор розмови (`session_id`)\n\n`session_id` — ключ до історії розмови. Вимоги:\n\n* **Непередбачуваний і унікальний у межах бота.** Рекомендовано UUIDv4: `uuidgen`, `crypto.randomUUID()`, `uuid.uuid4()`.\n* **Не використовуйте id користувача, email, телефон, номер замовлення чи лічильники** (`user-42`, `client_7`). Передбачувані id стикаються між вашими власними користувачами — двоє різних людей потрапляють в одну розмову і бачать чужі повідомлення. (Крос-ботового витоку немає: сесія закріплюється за ботом при першому використанні, і чужий ключ отримає порожню відповідь або 404. Але колізії у межах вашого бота — ваша відповідальність.)\n* Одна розмова = один `session_id`, для всіх ендпоїнтів (`/chat`, `/handoff`, `/messages`, `/typing`).\n* Якщо не передати — сервер згенерує випадковий і поверне у полі `session_id` відповіді. Збережіть його на своєму боці.\n\nПотрібен звʼязок із вашим користувачем? Для цього є окреме поле `user_token` у `POST /v1/api/chat` — саме воно, а не `session_id`.\n\n## Ліміти запитів\n\nЛіміти рахуються у фіксованому вікні 1 хвилина, окремо для кожної пари (ключ + IP):\n\n| Група | Ендпоїнти | Ліміт |\n| --- | --- | --- |\n| Діалог і записи | `POST /chat`, `POST /chat/stream`, `POST /handoff`, `POST /knowledge`, `DELETE /knowledge/{id}` | **60 запитів/хв** |\n| Читання і сигнали | `GET /messages`, `GET /knowledge`, `GET /knowledge/{id}`, `POST /typing`, усі `/operator/*` | **240 запитів/хв** |\n\nЦе два незалежні бюджети — інтенсивний полінг не зʼїдає ліміт діалогу. Перевищення → `429` з кодом `rate_limited`; повторіть за кілька секунд з експоненційною витримкою. Полінг `GET /messages` раз на 2–3 секунди (20–30 запитів/хв) вкладається із запасом. На `/v1/admin/*` ліміти не застосовуються.\n\n## Формат помилок\n\nКожна помилка — JSON `{\"error\": \"…\", \"code\": \"…\"}`.\n\n* `error` — текст для людини. Може змінюватися будь-коли, **не будуйте на ньому логіку**.\n* `code` — стабільний машинний код. Розгалужуйтесь по ньому (або по HTTP-статусу).\n\nКоди, які реально повертає це API:\n\n| Код | Статус | Коли |\n| --- | --- | --- |\n| `bad_request` | 400 | Відсутнє або порожнє обовʼязкове поле. |\n| `missing_key` | 401 | Немає заголовка `X-Bot-Key`. |\n| `invalid_key` | 401 | Ключ невідомий або відкликаний. |\n| `wrong_key_type` | 403 | Переданий `pk_`-ключ там, де потрібен `sk_`. |\n| `not_found` | 404 | Розмова/джерело не існує або належить іншому боту; handoff вимкнено. |\n| `conflict` | 409 | Операція вже виконується (сканування сайту). |\n| `rate_limited` | 429 | Перевищено ліміт запитів. |\n| `internal_error` | 500 | Внутрішня помилка. Безпечно повторити. |\n| `llm_unavailable` | 503 | ШІ-бекенд тимчасово недоступний. Повторіть пізніше. |\n| `service_unavailable` | 503 | Інша тимчасова недоступність. |\n\nДва винятки, про які варто знати наперед:\n\n1. **`quota_exceeded` — не помилка.** Вичерпаний місячний ліміт розмов повертається як `200` з `\"quota_exceeded\": true` у тілі `POST /v1/api/chat` (див. `ChatResponse`).\n2. **У потоці `/v1/api/chat/stream`** кадр помилки містить `code` лише для `llm_unavailable`; кадр внутрішньої помилки приходить як `{\"error\":\"internal error\"}` **без** `code`. Тому в стрімі визначайте помилку за наявністю поля `error`, а не за `code`.\n\n## Ідемпотентність\n\nКожен запис (`POST /chat`, `/chat/stream`, `/handoff`, `/knowledge`, `/operator/threads/{sid}/reply`) приймає заголовок `Idempotency-Key`:\n\n```\nIdempotency-Key: 3f6c1b9e-7f42-4d2a-9a6f-0d5b1f0b21c7\n```\n\n* Перший запит виконується звичайно, а його відповідь зберігається на **24 години**.\n* Повтор із тим самим ключем **не виконує дію вдруге** — повертається збережена відповідь із заголовком `Idempotency-Replayed: true`. Так ретрай після мережевого таймауту не спалить токени вдруге, не продублює репліку в транскрипті й не надішле другий вебхук.\n* Ключ дійсний у межах вашого API-ключа: чужі ключі ніколи не перетинаються з вашими.\n* Той самий ключ з **іншим** тілом запиту → `409` (`idempotency_key_reused`). Якщо перша спроба ще виконується → `409` (`idempotency_in_progress`), повторіть за секунду.\n* Помилкові відповіді (5xx, 429) не зберігаються — повтор нормально дійде до сервера.\n\nГенеруйте ключ на кожну **логічну** операцію (UUIDv4) і перевикористовуйте його саме для ретраїв цієї операції.\n\n## Пагінація\n\n`GET /operator/threads`, `GET /operator/threads/{sid}` і `GET /knowledge` віддають дані сторінками:\n\n* `?limit=` — розмір сторінки (1–200, типово 50); більше за 200 обрізається.\n* `?cursor=` — непрозорий курсор із поля `next_cursor` попередньої відповіді.\n* Порожній `next_cursor` означає «це остання сторінка».\n\nФормат курсора — деталь реалізації; не розбирайте й не конструюйте його самі.\n\n## Обмеження розміру запиту\n\nТіло запиту обмежене **1 МіБ** для всіх `/v1/api/*`, окрім `POST /v1/api/knowledge` — там **5 МіБ**. Перевищення → `413` з кодом `payload_too_large`. Крім того: `message` — до 8000 символів, `session_id` — до 128 символів (інакше `400`).\n\n## Вихідні вебхуки: перевірка підпису\n\nЯкщо для агента налаштовано webhook-адресу, ми надсилаємо на неї `POST` при трьох подіях передачі людині: `escalated`, `visitor_message` і `resolved`. Схема тіла й приклади — у розділі **Webhooks** цього довідника.\n\nКожна доставка підписана:\n\n```\nX-YouSelfBot-Timestamp: 1753600000\nX-YouSelfBot-Signature: sha256=<hex>\n```\n\nПідпис — це `HMAC-SHA256(секрет, \"<timestamp>.<тіло запиту як є>\")`, у hex. Секрет агента — у кабінеті (поле `handoff_webhook_secret` конфігурації агента); він починається з `whsec_`.\n\nЯк перевіряти:\n\n1. Обчисліть HMAC над рядком `timestamp + \".\" + raw_body` (саме сирим тілом, до JSON-парсингу).\n2. Порівняйте з `X-YouSelfBot-Signature` (без префікса `sha256=`) **у сталий час** (`hmac.compare_digest`, `crypto.timingSafeEqual`).\n3. Відхиляйте доставки, старші за кілька хвилин, — так підписаний запит не можна відтворити пізніше.\n\n```python\nimport hashlib, hmac, time\n\ndef verify(raw_body: bytes, headers, secret: str, tolerance=300) -> bool:\n    ts = headers.get(\"X-YouSelfBot-Timestamp\", \"\")\n    got = headers.get(\"X-YouSelfBot-Signature\", \"\").removeprefix(\"sha256=\")\n    if not ts.isdigit() or abs(time.time() - int(ts)) > tolerance:\n        return False                       # застаріла доставка — не приймаємо\n    mac = hmac.new(secret.encode(), ts.encode() + b\".\" + raw_body, hashlib.sha256)\n    return hmac.compare_digest(mac.hexdigest(), got)\n```\n\nНевдалу доставку (5xx або мережева помилка) ми повторюємо тричі — через 1 с, 5 с і 25 с. Тому ваш обробник має бути **ідемпотентним**: та сама подія може прийти двічі. Відповідь `4xx` вважається остаточною і не повторюється. На доставку відводиться 5 секунд — відповідайте `200` одразу, а роботу робіть уже після.\n\nАдреса має бути публічною: запити у приватні мережі ми не робимо навмисно (захист від SSRF).\n\n## Вхідний вебхук: розбудити агента\n\nЗворотний напрямок. Ви створюєте в кабінеті тригер типу `webhook` — і дістаєте адресу `POST /v1/hooks/{triggerID}` та секрет (він показується **рівно один раз**, при створенні). Далі будь-яка ваша система може розбудити агента, підписавши тіло цим секретом:\n\n```\nX-Hook-Signature: sha256=<hex HMAC-SHA256(секрет, тіло)>\n```\n\nЗверніть увагу: тут підписується **тільки тіло**, без timestamp — на відміну від вихідних вебхуків вище. Деталі, режими `sync`/`async` і приклади — у розділі **Тригери**.\n\n## Підтвердження особи відвідувача\n\nПоле `user_token` ми не перевіряємо — воно призначене вашому ж API. Віджет працює у браузері відвідувача, тіло запиту складає сторінка, тож будь-яке значення в ньому може бути довільним. Саме тому за `user_token` не можна надати додаткових прав.\n\nЯкщо ви хочете, щоб КОНКРЕТНИЙ відвідувач мав доступ до дій, позначених «лише для адміністратора», ваш **сервер** підтверджує його особу секретом, який ми з вами поділяємо. Секрет бота — у кабінеті, на вкладці «Дії» (поле `visitor_identity_secret` конфігурації бота). Він має лишатись на вашому сервері й ніколи не потрапляти у браузер, сторінку чи мобільний застосунок.\n\nПідтвердження — рядок із трьох частин, розділених крапкою:\n\n```\nv1.<дані у base64url>.<HMAC-SHA256 у hex>\n```\n\n**Дані** — це JSON із чотирьох полів:\n\n| Поле | Що це |\n| --- | --- |\n| `user` | хто це — той самий ідентифікатор користувача, що ви передаєте в `user_token` |\n| `role` | `admin` для адміністратора, будь-що інше означає звичайного відвідувача |\n| `bot` | id бота, для якого видано підтвердження |\n| `exp` | момент, до якого воно дійсне (Unix-секунди) |\n\n**Підпис** — `HMAC-SHA256(секрет, \"v1.\" + <дані у base64url>)`, у hex. Підписується весь рядок разом із префіксом версії; base64url — без вирівнювальних `=`.\n\nПриклад (Python):\n\n```python\nimport base64, hashlib, hmac, json, time\n\nSECRET = \"idsec_…\"          # з кабінету; лише на сервері\nBOT    = \"bot_…\"\n\ndef confirm(user_id: str, role: str = \"admin\", ttl: int = 300) -> str:\n    data = {\"user\": user_id, \"role\": role, \"bot\": BOT, \"exp\": int(time.time()) + ttl}\n    payload = base64.urlsafe_b64encode(json.dumps(data).encode()).rstrip(b\"=\").decode()\n    signed = \"v1.\" + payload\n    sig = hmac.new(SECRET.encode(), signed.encode(), hashlib.sha256).hexdigest()\n    return signed + \".\" + sig\n```\n\nГотовий рядок віддайте своїй сторінці й передавайте у полі `user_auth` кожного повідомлення.\n\nНемає власного сервера? Обмежену дію все одно можна спробувати: у тестовому чаті в кабінеті ви вже підтверджені як адміністратор саме цього бота.\n\nЩо варто знати:\n\n* **Строк дії обовʼязковий і обмежений.** Максимум — **24 години**; підтвердження без `exp`, з простроченим або надто далеким `exp` не приймається. Підтвердження лежить у браузері, тож той, хто його скопіює, зможе ним скористатися до кінця строку — тому видавайте короткі (кілька хвилин) і оновлюйте їх.\n* **Прив'язка до бота.** Підтвердження, видане для одного бота, не працює для іншого — навіть у межах вашого ж кабінету.\n* **Помилки немає.** Відсутнє, зіпсоване, прострочене чи чуже підтвердження не перериває розмову: відвідувач просто лишається звичайним. Так сайт без цього налаштування працює як і раніше.\n* **`user` із підтвердження має пріоритет** над полем `user_token`: сторінка не може лишити чуже підтвердження й підставити власний ідентифікатор.\n* **Секрет змінюється — попередні підтвердження перестають діяти.** Плануйте оновлення секрету на час, коли це прийнятно.\n\n\n## Ідентифікатор запиту\n\nКожна відповідь містить заголовок `X-Request-Id` — короткий ідентифікатор саме цього запиту. Він є і на успішних відповідях, і на помилках.\n\n* Збережіть його у своїх логах поруч із власним записом про виклик. Якщо доведеться звертатися в підтримку, цей ідентифікатор одразу вкаже на потрібний запис у наших логах — без нього пошук іде за часом і описом, і це довше.\n* Можна надіслати свій `X-Request-Id` у запиті — тоді ми використаємо його замість власного, і один ідентифікатор проходить наскрізь через ваші системи й наші. Значення має бути коротким (до 64 символів) і складатися з латинських літер, цифр та `-` `_` `.` `:`; усе інше буде відкинуто, і ми згенеруємо свій.\n\n## Версіонування\n\n* Префікс шляху `/v1` фіксує контракт. Поки він у цій специфікації — він працює.\n* У межах `/v1` зміни лише **адитивні**: додаються нові ендпоїнти, нові поля у відповідях і нові необовʼязкові поля у запитах, зʼявляються нові значення `code` та `status`. Наявні поля не перейменовуються, не змінюють тип і не зникають.\n* Тому ваш клієнт **мусить ігнорувати незнайомі поля** й толерувати незнайомі значення enum (наприклад, новий `code`), а не падати на них. Поля з цієї специфікації додаються без попередження — це не breaking change.\n* Несумісна зміна = новий префікс (`/v2`), який працює **паралельно** з `/v1`; автоматичного перемикання не буде.\n* Застарівання: щонайменше **6 місяців** попередження — лист власникам ключів, позначка `deprecated` у цій специфікації та заголовки `Deprecation` і `Sunset` у відповідях відповідних ендпоїнтів.\n* Актуальна машинна версія контракту завжди тут: `GET https://api.youselfbot.com/v1/openapi.json`. Версія документа — у полі `info.version`."
  },
  "servers": [
    {
      "url": "https://api.youselfbot.com",
      "description": "YouSelfBot API"
    }
  ],
  "security": [
    {
      "apiKeyAuth": []
    }
  ],
  "tags": [
    {
      "name": "Chat",
      "description": "Діалог з ботом"
    },
    {
      "name": "Handoff",
      "description": "Передача розмови живому оператору"
    },
    {
      "name": "Knowledge",
      "description": "Навчання бази знань бота"
    },
    {
      "name": "Operator",
      "description": "Робота операторів через власний бекенд (interop)"
    },
    {
      "name": "Admin",
      "description": "Службові ендпоїнти наповнення бази знань (той самий секретний ключ)"
    },
    {
      "name": "Тригери",
      "description": "Вхідний вебхук: розбудити агента зі своєї системи"
    },
    {
      "name": "Кабінет · Агенти",
      "description": "Створення агентів, налаштування, ключі, статистика"
    },
    {
      "name": "Кабінет · Хід агента",
      "description": "Пайплайн як дані: чернетка, публікація, версії, пресети-оверлеї"
    },
    {
      "name": "Кабінет · Тригери",
      "description": "Розклад, вебхуки, полінг; запуски й стан агента"
    },
    {
      "name": "Кабінет · Знання",
      "description": "Джерела бази знань: тексти, сайти, файли, медіа"
    },
    {
      "name": "Кабінет · Дії",
      "description": "HTTP-дії агента: власні, з пресетів, з імпортованої документації"
    },
    {
      "name": "Кабінет · Звернення",
      "description": "Черга операторів: розмови, відповіді, закриття"
    },
    {
      "name": "Кабінет · Підключення",
      "description": "Telegram для операторів, зовнішній розробник"
    },
    {
      "name": "Кабінет · Команда",
      "description": "Учасники робочого простору, запрошення, ролі"
    },
    {
      "name": "Автентифікація",
      "description": "Реєстрація, вхід, сесії, пароль"
    },
    {
      "name": "Тарифікація",
      "description": "Тарифи, пакети кредитів, оплата, підписка"
    },
    {
      "name": "Віджет",
      "description": "Публічні виклики з браузера відвідувача (ключ pk_)"
    },
    {
      "name": "Службове",
      "description": "Проби, версія, специфікація, медіафайли"
    }
  ],
  "paths": {
    "/docs": {
      "get": {
        "tags": [
          "Службове"
        ],
        "summary": "Довідник API",
        "description": "Ця сторінка.",
        "security": [],
        "responses": {
          "200": {
            "description": "HTML."
          }
        }
      }
    },
    "/healthz": {
      "get": {
        "tags": [
          "Службове"
        ],
        "summary": "Проба життя",
        "description": "Чи живий процес. Навмисно не чіпає нічого зовнішнього: проба, що падає разом із базою, перезапустила б усі репліки одночасно й зробила б відновну аварію повною.",
        "security": [],
        "responses": {
          "200": {
            "description": "Живий.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "example": "ok"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/media/{id}": {
      "get": {
        "tags": [
          "Службове"
        ],
        "summary": "Медіафайл",
        "description": "Віддає завантажене зображення за постійним посиланням.",
        "security": [],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Файл."
          },
          "404": {
            "description": "Немає такого файла.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/readyz": {
      "get": {
        "tags": [
          "Службове"
        ],
        "summary": "Проба готовності",
        "description": "Чи може процес обслуговувати запити. Окремо від проби життя: недоступне сховище інакше виявляється лише як помилка на чиємусь вході.",
        "security": [],
        "responses": {
          "200": {
            "description": "Готовий."
          },
          "503": {
            "description": "Не готовий.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/admin/scrape": {
      "post": {
        "tags": [
          "Admin"
        ],
        "summary": "Проіндексувати сайт у базу знань",
        "description": "Сканує вказаний сайт і індексує його текст у базу знань бота. Сканування виконується **у фоні**: відповідь `202` повертається одразу, а сам обхід сторінок може тривати десятки секунд або хвилини.\n\nЕндпоїнт не повідомляє про завершення й не має власного статусу — прогрес видно в кабінеті YouSelfBot. Щоб переконатися, що текст проіндексовано, за кілька хвилин звірте `GET /v1/api/knowledge`.\n\nОдночасно для одного бота може виконуватися лише одне сканування: повторний запит, поки триває попереднє, дає `409`. Ліміти запитів на `/v1/admin/*` не застосовуються, тому `429` тут не буває.\n\nПриватні та внутрішні адреси блокуються (захист від SSRF); недоступний сайт, сайт без тексту або заблокований обхід не повертають помилку у HTTP — вони просто не додають нічого до бази.",
        "operationId": "adminScrape",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "url"
                ],
                "properties": {
                  "url": {
                    "type": "string",
                    "format": "uri",
                    "description": "Публічна URL-адреса сторінки або сайту."
                  },
                  "max_pages": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 50,
                    "default": 1,
                    "description": "Скільки сторінок обійти. Менше за 1 → 1; більше за 50 → 50. Значення поза межами 1…50 обрізається до найближчої межі."
                  }
                }
              },
              "examples": {
                "singlePage": {
                  "summary": "Одна сторінка",
                  "value": {
                    "url": "https://example.com/faq"
                  }
                },
                "crawl": {
                  "summary": "Обхід до 20 сторінок",
                  "value": {
                    "url": "https://example.com",
                    "max_pages": 20
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Сканування запущено у фоні.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "status",
                    "url"
                  ],
                  "properties": {
                    "status": {
                      "type": "string",
                      "const": "scanning"
                    },
                    "url": {
                      "type": "string"
                    }
                  }
                },
                "examples": {
                  "started": {
                    "value": {
                      "status": "scanning",
                      "url": "https://example.com/faq"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "description": "Сканування цього бота вже триває. Дочекайтеся завершення.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "default": {
                    "value": {
                      "error": "сканування цього бота вже триває",
                      "code": "conflict"
                    }
                  }
                }
              }
            }
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          }
        }
      }
    },
    "/v1/admin/sources": {
      "post": {
        "tags": [
          "Admin"
        ],
        "summary": "Навчити базу знань (службовий аліас)",
        "description": "Технічно ідентичний до `POST /v1/api/knowledge`: той самий секретний ключ, те саме тіло, та сама відповідь. Залишений для сумісності зі старішими інтеграціями.\n\n**Для нових інтеграцій використовуйте `POST /v1/api/knowledge`** — саме там живуть решта операцій із базою знань (перелік, читання, видалення).\n\nВідмінність лише одна: на `/v1/admin/*` не застосовуються ліміти запитів, тому `429` тут не буває.",
        "operationId": "adminAddSource",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "text"
                ],
                "properties": {
                  "source_id": {
                    "type": "string",
                    "description": "Стабільний ID фрагмента (для оновлення). Опційно."
                  },
                  "title": {
                    "type": "string",
                    "description": "Назва джерела."
                  },
                  "text": {
                    "type": "string",
                    "description": "Текст, який бот має вивчити."
                  }
                }
              },
              "examples": {
                "faq": {
                  "value": {
                    "title": "Гарантія",
                    "text": "Гарантія 24 місяці на всю техніку."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Джерело прийнято на індексацію.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "status",
                    "source_id"
                  ],
                  "properties": {
                    "status": {
                      "type": "string",
                      "const": "indexed"
                    },
                    "source_id": {
                      "type": "string"
                    }
                  }
                },
                "examples": {
                  "accepted": {
                    "value": {
                      "status": "indexed",
                      "source_id": "a3f19c7b21"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/v1/api/chat": {
      "post": {
        "tags": [
          "Chat"
        ],
        "summary": "Надіслати повідомлення боту",
        "description": "Одна репліка діалогу. Відповідь бота повертається повністю (без стріму).\n\n`session_id` тримайте однаковим у межах однієї розмови — так бот памʼятає контекст. Якщо не передати, сервер згенерує новий і поверне його у відповіді.\n\nВідповідь `200` має **три різні режими**, і всі приходять з тим самим статусом:\n\n1. **Звичайна відповідь бота** — є `answer` і `session_id`.\n2. **Розмову веде людина** — `\"handoff\": true`. Бот навмисно мовчить. `answer` містить підтвердження ескалації (`«Передаю вас оператору…»`) або **порожній рядок**, якщо оператор уже на звʼязку і повідомлення просто переслано йому. Порожній `answer` не показуйте як репліку. Далі опитуйте `GET /v1/api/messages`.\n3. **Вичерпано місячний ліміт розмов** — `\"quota_exceeded\": true`, `answer` містить текст-заглушку для клієнта. Нові розмови не стартують до наступного періоду або поповнення; уже розпочаті продовжують працювати.\n\nОбидва прапорці відсутні у звичайній відповіді — перевіряйте їх як `resp.handoff === true`.\n\n**`notice`.** Якщо у відповіді є `\"notice\": \"outage\"`, текст у `answer` — не репліка бота, а службове повідомлення про несправність або ліміт (напр. база знань недоступна). Показуйте його окремо від відповідей і не пропонуйте оцінити: інакше клієнт оцінює збій, а ви отримуєте зворотний звʼязок про відповідь, якої не було.\n\n**Перевантаження.** Якщо ШІ-бекенд зайнятий або хід не встиг завершитись, приходить `503` з `code: \"llm_overloaded\"` і заголовком `Retry-After` — на відміну від `llm_unavailable`, такий запит має сенс повторити.\n\n**Ідемпотентність.** Додайте заголовок `Idempotency-Key` — і повтор після таймауту не виконає дію вдруге: повернеться збережена відповідь першої спроби з `Idempotency-Replayed: true` (ключ живе 24 години).",
        "operationId": "chat",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ChatRequest"
              },
              "examples": {
                "simple": {
                  "summary": "Проста репліка",
                  "value": {
                    "session_id": "3f6c1b9e-7f42-4d2a-9a6f-0d5b1f0b21c7",
                    "message": "Які у вас години роботи?"
                  }
                },
                "withContext": {
                  "summary": "З контекстом сторінки та ідентифікатором клієнта",
                  "value": {
                    "session_id": "3f6c1b9e-7f42-4d2a-9a6f-0d5b1f0b21c7",
                    "message": "Чи є ця модель у синьому кольорі?",
                    "user_token": "crm-customer-8891",
                    "page_context": "Сторінка товару: Кавоварка Aurora X2, 8 490 грн, у наявності"
                  }
                },
                "escalate": {
                  "summary": "Кнопка «оператор» — одразу в чергу до людини",
                  "value": {
                    "session_id": "3f6c1b9e-7f42-4d2a-9a6f-0d5b1f0b21c7",
                    "message": "Хочу поговорити з людиною",
                    "escalate": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Відповідь бота, підтвердження ескалації (`handoff`) або вичерпаний ліміт (`quota_exceeded`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ChatResponse"
                },
                "examples": {
                  "answer": {
                    "summary": "Звичайна відповідь бота",
                    "value": {
                      "answer": "Ми працюємо з 9:00 до 18:00, пн–пт.",
                      "session_id": "3f6c1b9e-7f42-4d2a-9a6f-0d5b1f0b21c7"
                    }
                  },
                  "handoffQueued": {
                    "summary": "Ескалація: клієнта поставлено в чергу",
                    "value": {
                      "answer": "Передаю вас оператору. Зачекайте, будь ласка — він відповість у цьому чаті.",
                      "session_id": "3f6c1b9e-7f42-4d2a-9a6f-0d5b1f0b21c7",
                      "handoff": true
                    }
                  },
                  "handoffLive": {
                    "summary": "Оператор уже на звʼязку: повідомлення переслано, показувати нічого",
                    "value": {
                      "answer": "",
                      "session_id": "3f6c1b9e-7f42-4d2a-9a6f-0d5b1f0b21c7",
                      "handoff": true
                    }
                  },
                  "quotaExceeded": {
                    "summary": "Вичерпано місячний ліміт розмов",
                    "value": {
                      "answer": "Вибачте, бот тимчасово недоступний. Спробуйте, будь ласка, пізніше.",
                      "session_id": "3f6c1b9e-7f42-4d2a-9a6f-0d5b1f0b21c7",
                      "quota_exceeded": true
                    }
                  }
                }
              }
            },
            "headers": {
              "Idempotency-Replayed": {
                "schema": {
                  "type": "string",
                  "enum": [
                    "true"
                  ]
                },
                "description": "Присутній лише коли це відтворена відповідь попереднього запиту з тим самим `Idempotency-Key` (дія НЕ виконувалася повторно)."
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "$ref": "#/components/responses/IdempotencyConflict"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "description": "ШІ-бекенд недоступний (`llm_unavailable` — ключ або кошти, потрібні дії власника) або перевантажений (`llm_overloaded` — має сенс повторити через `Retry-After`). Розрізняйте за `code`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "unavailable": {
                    "summary": "Ключ невалідний або вичерпані кошти",
                    "value": {
                      "error": "Вибачте, асистент тимчасово недоступний. Ми вже знаємо про це — спробуйте, будь ласка, трохи згодом.",
                      "code": "llm_unavailable",
                      "session_id": "3f6c1b9e-7f42-4d2a-9a6f-0d5b1f0b21c7"
                    }
                  },
                  "overloaded": {
                    "summary": "Перевантаження — повторіть через Retry-After",
                    "value": {
                      "error": "Вибачте, зараз надто багато запитів і відповідь не встигла надійти. Надішліть, будь ласка, повідомлення ще раз.",
                      "code": "llm_overloaded",
                      "retry_after": 20,
                      "session_id": "3f6c1b9e-7f42-4d2a-9a6f-0d5b1f0b21c7"
                    }
                  }
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "Скільки секунд зачекати перед повтором. Приходить лише з `llm_overloaded`.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ]
      }
    },
    "/v1/api/chat/stream": {
      "post": {
        "tags": [
          "Chat"
        ],
        "summary": "Надіслати повідомлення (потокова відповідь, SSE)",
        "description": "Те саме, що `POST /v1/api/chat`, але відповідь стрімиться через Server-Sent Events (`Content-Type: text/event-stream`). Кожен рядок має вигляд `data: <json>` і завершується порожнім рядком.\n\n**Види кадрів** (розрізняйте за наявним полем):\n\n| Кадр | Поля | Значення |\n| --- | --- | --- |\n| фрагмент | `delta`, іноді `notice` | Черговий шматок тексту. Дописуйте до вже отриманого. |\n| помилка | `error`, іноді `code` і `retry_after` | Генерація зірвалась. Приходить **після** заголовків, тому HTTP-статус лишається `200`. |\n| кінець | `done`, `session_id`, `handoff`, `quota_exceeded`, іноді `notice` | Останній кадр. Надсилається **завжди**, у тому числі після кадру помилки. |\n\n```\ndata: {\"delta\":\"Ми працюємо \"}\n\ndata: {\"delta\":\"з 9:00 до 18:00.\"}\n\ndata: {\"done\":true,\"session_id\":\"3f6c1b9e-7f42-4d2a-9a6f-0d5b1f0b21c7\",\"handoff\":false,\"quota_exceeded\":false}\n\n```\n\nКадр помилки буває трьох виглядів:\n\n* `{\"error\":\"Вибачте, асистент тимчасово недоступний…\",\"code\":\"llm_unavailable\"}` — ШІ-бекенд недоступний (ключ або кошти); повтор не допоможе;\n* `{\"error\":\"Вибачте, зараз надто багато запитів…\",\"code\":\"llm_overloaded\",\"retry_after\":20}` — перевантаження або вичерпаний час ходу; повторіть через `retry_after` секунд;\n* `{\"error\":\"internal error\"}` — внутрішня помилка, **без** поля `code`.\n\nТому перевіряйте саме `if (frame.error)`, а не `frame.code`.\n\n### Службові повідомлення (`notice`)\n\nКадр з `\"notice\": \"outage\"` означає, що текст цього ходу — не репліка бота, а повідомлення про несправність чи вичерпаний ліміт. Текст усе одно приходить у `delta` (клієнти, написані до появи поля, працюють як раніше), а маркер дублюється у кінцевому кадрі — бо частину таких повідомлень видно лише після завершення ходу. Показуйте їх окремо від відповідей і без оцінок.\n\n### Відмінності від `/v1/api/chat`\n\nЕскалація і вичерпаний ліміт приходять звичайним кадром `delta` (а коли оператор уже на звʼязку — не приходить жодного `delta`, лише `done`). Стан ходу читайте з кінцевого кадру: `handoff` і `quota_exceeded` там ті самі, що й у `POST /v1/api/chat`.\n\nПомилки автентифікації, ліміту та валідації повертаються **до** початку стріму — звичайним JSON із відповідним статусом.\n\n**Ідемпотентність.** Додайте заголовок `Idempotency-Key` — і повтор після таймауту не виконає дію вдруге: повернеться збережена відповідь першої спроби з `Idempotency-Replayed: true` (ключ живе 24 години).",
        "operationId": "chatStream",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ChatRequest"
              },
              "examples": {
                "simple": {
                  "summary": "Проста репліка",
                  "value": {
                    "session_id": "3f6c1b9e-7f42-4d2a-9a6f-0d5b1f0b21c7",
                    "message": "Розкажи про доставку"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Потік Server-Sent Events. Схема нижче описує **один кадр** — тобто JSON у полі `data:`; фізично тіло відповіді є послідовністю таких кадрів.",
            "content": {
              "text/event-stream": {
                "schema": {
                  "$ref": "#/components/schemas/StreamFrame"
                }
              }
            },
            "headers": {
              "Idempotency-Replayed": {
                "schema": {
                  "type": "string",
                  "enum": [
                    "true"
                  ]
                },
                "description": "Присутній лише коли це відтворена відповідь попереднього запиту з тим самим `Idempotency-Key` (дія НЕ виконувалася повторно)."
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "$ref": "#/components/responses/IdempotencyConflict"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ]
      }
    },
    "/v1/api/handoff": {
      "post": {
        "tags": [
          "Handoff"
        ],
        "summary": "Передати розмову живому оператору",
        "description": "Ставить сесію в чергу до оператора (статус `waiting`). Після цього опитуйте `GET /v1/api/messages`, щоб отримати відповіді оператора та показати їх у власному чаті.\n\nТе саме роблять `POST /v1/api/chat` з `\"escalate\": true` та явне прохання клієнта про оператора у тексті — цей ендпоїнт потрібен, коли ескалація йде без нової репліки (наприклад, кнопка «Звʼязатися з людиною»).\n\n**Ідемпотентність.** Додайте заголовок `Idempotency-Key` — і повтор після таймауту не виконає дію вдруге: повернеться збережена відповідь першої спроби з `Idempotency-Replayed: true` (ключ живе 24 години).",
        "operationId": "handoff",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "session_id"
                ],
                "properties": {
                  "session_id": {
                    "$ref": "#/components/schemas/SessionID"
                  }
                }
              },
              "examples": {
                "simple": {
                  "value": {
                    "session_id": "3f6c1b9e-7f42-4d2a-9a6f-0d5b1f0b21c7"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Сесію поставлено в чергу до оператора.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "const": "waiting"
                    }
                  }
                },
                "examples": {
                  "queued": {
                    "value": {
                      "status": "waiting"
                    }
                  }
                }
              }
            },
            "headers": {
              "Idempotency-Replayed": {
                "schema": {
                  "type": "string",
                  "enum": [
                    "true"
                  ]
                },
                "description": "Присутній лише коли це відтворена відповідь попереднього запиту з тим самим `Idempotency-Key` (дія НЕ виконувалася повторно)."
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "Handoff недоступний: вимкнено для цього бота, або сесія належить іншому боту.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "default": {
                    "value": {
                      "error": "operator handoff disabled for this bot",
                      "code": "not_found"
                    }
                  }
                }
              }
            }
          },
          "409": {
            "$ref": "#/components/responses/IdempotencyConflict"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ]
      }
    },
    "/v1/api/knowledge": {
      "post": {
        "tags": [
          "Knowledge"
        ],
        "summary": "Навчити базу знань бота",
        "description": "Індексує довільний текст у базу знань бота (RAG). Передайте стабільний `source_id`, щоб оновлювати той самий фрагмент; без нього щоразу створюється новий. Використовуйте для синхронізації FAQ, товарів, документації тощо зі свого боку.\n\n**Ідемпотентність.** Додайте заголовок `Idempotency-Key` — і повтор після таймауту не виконає дію вдруге: повернеться збережена відповідь першої спроби з `Idempotency-Replayed: true` (ключ живе 24 години).",
        "operationId": "teachKnowledge",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "text"
                ],
                "properties": {
                  "source_id": {
                    "type": "string",
                    "description": "Стабільний ID фрагмента (для оновлення). Опційно."
                  },
                  "title": {
                    "type": "string",
                    "description": "Назва джерела (для посилань у відповідях)."
                  },
                  "text": {
                    "type": "string",
                    "description": "Текст, який бот має вивчити."
                  }
                }
              },
              "examples": {
                "faq": {
                  "summary": "Новий фрагмент FAQ",
                  "value": {
                    "title": "Графік роботи",
                    "text": "Ми працюємо з 9:00 до 18:00, пн–пт. У суботу — до 14:00. Неділя — вихідний."
                  }
                },
                "upsert": {
                  "summary": "Оновлення наявного джерела за стабільним id",
                  "value": {
                    "source_id": "faq-delivery",
                    "title": "Доставка",
                    "text": "Доставляємо Новою поштою за 1–2 дні. Від 2 000 грн — безкоштовно."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Джерело прийнято на індексацію. Кеш відповідей бота скинуто.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "status",
                    "source_id"
                  ],
                  "properties": {
                    "status": {
                      "type": "string",
                      "const": "indexed"
                    },
                    "source_id": {
                      "type": "string",
                      "description": "Переданий `source_id` або згенерований, якщо його не було."
                    }
                  }
                },
                "examples": {
                  "accepted": {
                    "value": {
                      "status": "indexed",
                      "source_id": "faq-delivery"
                    }
                  }
                }
              }
            },
            "headers": {
              "Idempotency-Replayed": {
                "schema": {
                  "type": "string",
                  "enum": [
                    "true"
                  ]
                },
                "description": "Присутній лише коли це відтворена відповідь попереднього запиту з тим самим `Idempotency-Key` (дія НЕ виконувалася повторно)."
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "$ref": "#/components/responses/IdempotencyConflict"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ]
      },
      "get": {
        "tags": [
          "Knowledge"
        ],
        "summary": "Перелічити джерела бази знань",
        "description": "Що саме проіндексовано для цього бота: назва, ідентифікатор і кількість фрагментів кожного джерела. Зручно, щоб звірити свою систему з базою бота перед синхронізацією.\n\n**Сторінками.** Відповідь обмежена `?limit=` (типово 50, максимум 200). Якщо `next_cursor` непорожній — є ще дані: повторіть запит із `?cursor=<next_cursor>`. Без параметрів отримаєте першу сторінку у звичному форматі.",
        "operationId": "listKnowledge",
        "responses": {
          "200": {
            "description": "Перелік джерел (порожній масив, якщо базу ще не наповнено).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "sources"
                  ],
                  "properties": {
                    "sources": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Source"
                      }
                    },
                    "next_cursor": {
                      "type": "string",
                      "description": "Курсор наступної сторінки. Порожній рядок — сторінок більше немає. Передайте його як `?cursor=`, щоб отримати продовження."
                    }
                  }
                },
                "examples": {
                  "list": {
                    "value": {
                      "sources": [
                        {
                          "id": "faq-delivery",
                          "title": "Доставка",
                          "chunks": 2
                        },
                        {
                          "id": "a3f19c7b21",
                          "title": "Графік роботи",
                          "chunks": 1
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/PageLimit"
          },
          {
            "$ref": "#/components/parameters/PageCursor"
          }
        ]
      }
    },
    "/v1/api/knowledge/{id}": {
      "get": {
        "tags": [
          "Knowledge"
        ],
        "summary": "Прочитати джерело повністю",
        "description": "Повертає збережений текст джерела без скорочень — на відміну від відповіді бота, яка спирається лише на релевантні фрагменти.",
        "operationId": "readKnowledge",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Ідентифікатор джерела зі списку.",
            "example": "faq-delivery"
          }
        ],
        "responses": {
          "200": {
            "description": "Джерело.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "id",
                    "title",
                    "text"
                  ],
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "title": {
                      "type": "string"
                    },
                    "text": {
                      "type": "string"
                    }
                  }
                },
                "examples": {
                  "source": {
                    "value": {
                      "id": "faq-delivery",
                      "title": "Доставка",
                      "text": "Доставляємо Новою поштою за 1–2 дні. Від 2 000 грн — безкоштовно."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "Джерело не знайдено або належить іншому боту.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "default": {
                    "value": {
                      "error": "source not found",
                      "code": "not_found"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "delete": {
        "tags": [
          "Knowledge"
        ],
        "summary": "Видалити джерело",
        "description": "Прибирає джерело з бази знань і скидає кеш відповідей, побудованих на ньому.",
        "operationId": "deleteKnowledge",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Ідентифікатор джерела.",
            "example": "faq-delivery"
          }
        ],
        "responses": {
          "200": {
            "description": "Видалено. Ідемпотентно: видалення неіснуючого id теж повертає `200`.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "status",
                    "id"
                  ],
                  "properties": {
                    "status": {
                      "type": "string",
                      "const": "deleted"
                    },
                    "id": {
                      "type": "string"
                    }
                  }
                },
                "examples": {
                  "deleted": {
                    "value": {
                      "status": "deleted",
                      "id": "faq-delivery"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "База знань недоступна для цього бота.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "default": {
                    "value": {
                      "error": "knowledge unavailable",
                      "code": "not_found"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/v1/api/messages": {
      "get": {
        "tags": [
          "Handoff"
        ],
        "summary": "Отримати нові повідомлення оператора",
        "description": "Повертає повідомлення живого оператора для сесії, новіші за мітку `after`, і поточний статус розмови. Опитуйте періодично (раз на 2–3 с), поки `status` = `waiting` або `operator`.\n\n**Курсор.** Після кожної відповіді збережіть `at` **останнього** отриманого повідомлення і передайте його як `after` у наступному запиті. На першому запиті передавайте `after=0` або не передавайте зовсім. Без просування курсора кожен запит повертатиме всю історію знову, і повідомлення дублюватимуться у вашому чаті.\n\nНевідома сесія (або сесія іншого бота) — це не помилка: повертається `200` з порожнім `messages` і `status: \"bot\"`.\n\n**Точний курсор.** Замість `after` передавайте `?cursor=` зі значенням `next_cursor` з попередньої відповіді. `after` — це лише мітка часу в мілісекундах: якщо оператор надіслав дві репліки в одну мілісекунду, друга з них ніколи не потрапить у вибірку `after`. `cursor` враховує пару (час, id) і не губить повідомлень. Параметр `after` продовжує працювати як раніше.",
        "operationId": "messages",
        "parameters": [
          {
            "name": "session_id",
            "in": "query",
            "required": true,
            "schema": {
              "$ref": "#/components/schemas/SessionID"
            },
            "description": "Ідентифікатор розмови.",
            "example": "3f6c1b9e-7f42-4d2a-9a6f-0d5b1f0b21c7"
          },
          {
            "name": "after",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "format": "int64",
              "minimum": 0,
              "default": 0
            },
            "description": "Повертати лише повідомлення, новіші за цю мітку часу (Unix ms). Передавайте значення поля **`at`** останнього отриманого повідомлення (`messages[messages.length - 1].at`). Перший запит — `0`.",
            "example": 1753600000123
          },
          {
            "$ref": "#/components/parameters/PageCursor"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 200
            },
            "description": "Скільки повідомлень повернути. **Увага:** тут дефолт — 200, а не 50, як в інших посторінкових ендпоїнтах: опитування має віддавати все накопичене за час паузи. Більші значення обрізаються до 200."
          }
        ],
        "responses": {
          "200": {
            "description": "Нові повідомлення оператора, статус розмови та ознака «оператор друкує».",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "messages",
                    "status"
                  ],
                  "properties": {
                    "messages": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Message"
                      },
                      "description": "Лише повідомлення ОПЕРАТОРА, новіші за `after`, у хронологічному порядку."
                    },
                    "status": {
                      "$ref": "#/components/schemas/HandoffStatus"
                    },
                    "operator_typing": {
                      "type": "boolean",
                      "description": "`true` → оператор набирає повідомлення просто зараз. Показуйте у своєму чаті «оператор друкує…». Сигнал згасає сам приблизно через 6 секунд, тож просто перемальовуйте індикатор за значенням з кожного опитування. Поле відсутнє, якщо передача людині для цього агента недоступна — трактуйте відсутність як `false`."
                    },
                    "next_cursor": {
                      "type": "string",
                      "description": "Курсор для НАСТУПНОГО опитування. Передавайте його як `?cursor=` — на відміну від `after`, він розрізняє повідомлення, надіслані в ту саму мілісекунду."
                    },
                    "tasks": {
                      "type": "array",
                      "description": "Довгі дії агента, які виконуються просто зараз (наприклад, звернення до вашої системи, що триває хвилини). Показуйте як картки прогресу поруч із чатом. Порожній масив — нічого не виконується.",
                      "items": {
                        "$ref": "#/components/schemas/Task"
                      }
                    }
                  }
                },
                "examples": {
                  "newMessage": {
                    "summary": "Нова відповідь оператора",
                    "value": {
                      "messages": [
                        {
                          "id": "8f1c0b2a4e",
                          "author": "operator",
                          "text": "Вітаю! Так, доставимо завтра.",
                          "at": 1753600000123
                        }
                      ],
                      "status": "operator",
                      "operator_typing": false
                    }
                  },
                  "typing": {
                    "summary": "Нічого нового, оператор друкує",
                    "value": {
                      "messages": [],
                      "status": "operator",
                      "operator_typing": true
                    }
                  },
                  "waiting": {
                    "summary": "У черзі, оператор ще не підключився",
                    "value": {
                      "messages": [],
                      "status": "waiting",
                      "operator_typing": false
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/api/operator/threads": {
      "get": {
        "tags": [
          "Operator"
        ],
        "summary": "Список звернень до оператора",
        "description": "Повертає розмови бота, що чекають на оператора або вже ведуться ним. Використовуйте у власному дашборді оператора, щоб підключити своїх агентів до нашого бота.\n\n**Сторінками.** Відповідь обмежена `?limit=` (типово 50, максимум 200). Якщо `next_cursor` непорожній — є ще дані: повторіть запит із `?cursor=<next_cursor>`. Без параметрів отримаєте першу сторінку у звичному форматі.",
        "operationId": "operatorThreads",
        "responses": {
          "200": {
            "description": "Список звернень разом із живими сигналами набору тексту.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "threads",
                    "typing",
                    "drafts"
                  ],
                  "properties": {
                    "threads": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Thread"
                      }
                    },
                    "typing": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "`session_id` тредів, де відвідувач друкує просто зараз."
                    },
                    "drafts": {
                      "type": "object",
                      "additionalProperties": {
                        "type": "string"
                      },
                      "description": "Чернетки відвідувачів: `session_id` → недонабраний текст (до 500 символів). Присутні лише для тредів, де відвідувач друкує і надсилає превʼю (поле `text` у `POST /v1/api/typing`). Тред може бути у `typing` без запису в `drafts`."
                    },
                    "next_cursor": {
                      "type": "string",
                      "description": "Курсор наступної сторінки. Порожній рядок — сторінок більше немає. Передайте його як `?cursor=`, щоб отримати продовження."
                    }
                  }
                },
                "examples": {
                  "threads": {
                    "value": {
                      "threads": [
                        {
                          "session_id": "3f6c1b9e-7f42-4d2a-9a6f-0d5b1f0b21c7",
                          "bot_id": "bot_9f2c",
                          "status": "waiting",
                          "last_text": "Хочу поговорити з людиною",
                          "updated_at": 1753600000123
                        }
                      ],
                      "typing": [
                        "3f6c1b9e-7f42-4d2a-9a6f-0d5b1f0b21c7"
                      ],
                      "drafts": {
                        "3f6c1b9e-7f42-4d2a-9a6f-0d5b1f0b21c7": "чи можна замовити на суб"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "Handoff недоступний для цього бота.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "default": {
                    "value": {
                      "error": "handoff unavailable",
                      "code": "not_found"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/PageLimit"
          },
          {
            "$ref": "#/components/parameters/PageCursor"
          }
        ]
      }
    },
    "/v1/api/operator/threads/{sid}": {
      "get": {
        "tags": [
          "Operator"
        ],
        "summary": "Повний транскрипт звернення",
        "description": "Усі повідомлення розмови — відвідувача, бота й оператора — у хронологічному порядку.\n\n**Сторінками.** Відповідь обмежена `?limit=` (типово 50, максимум 200). Якщо `next_cursor` непорожній — є ще дані: повторіть запит із `?cursor=<next_cursor>`. Без параметрів отримаєте першу сторінку у звичному форматі. Повідомлення йдуть від найстаріших до найновіших.",
        "operationId": "operatorTranscript",
        "parameters": [
          {
            "name": "sid",
            "in": "path",
            "required": true,
            "schema": {
              "$ref": "#/components/schemas/SessionID"
            },
            "description": "Ідентифікатор розмови (`session_id`).",
            "example": "3f6c1b9e-7f42-4d2a-9a6f-0d5b1f0b21c7"
          },
          {
            "$ref": "#/components/parameters/PageLimit"
          },
          {
            "$ref": "#/components/parameters/PageCursor"
          }
        ],
        "responses": {
          "200": {
            "description": "Усі повідомлення розмови та живий стан набору з боку відвідувача.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "messages",
                    "visitor_typing",
                    "visitor_draft"
                  ],
                  "properties": {
                    "messages": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Message"
                      }
                    },
                    "visitor_typing": {
                      "type": "boolean",
                      "description": "Чи набирає відвідувач повідомлення просто зараз."
                    },
                    "visitor_draft": {
                      "type": "string",
                      "description": "Чернетка відвідувача — недонабраний текст (до 500 символів), якщо превʼю ввімкнено й воно надсилається. Порожній рядок, якщо чернетки немає. Показуйте оператору приглушено, як «набирає: …», щоб її не сплутали з надісланим повідомленням."
                    },
                    "next_cursor": {
                      "type": "string",
                      "description": "Курсор наступної сторінки. Порожній рядок — сторінок більше немає. Передайте його як `?cursor=`, щоб отримати продовження."
                    }
                  }
                },
                "examples": {
                  "transcript": {
                    "value": {
                      "messages": [
                        {
                          "id": "1a2b3c4d5e",
                          "author": "visitor",
                          "text": "Хочу поговорити з людиною",
                          "at": 1753599990000
                        },
                        {
                          "id": "8f1c0b2a4e",
                          "author": "operator",
                          "text": "Вітаю! Слухаю вас.",
                          "at": 1753600000123
                        }
                      ],
                      "visitor_typing": true,
                      "visitor_draft": "чи можна замовити на суб"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "Звернення не знайдено або належить іншому боту.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "default": {
                    "value": {
                      "error": "thread not found",
                      "code": "not_found"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/v1/api/operator/threads/{sid}/reply": {
      "post": {
        "tags": [
          "Operator"
        ],
        "summary": "Відповідь оператора у розмову",
        "description": "Надсилає повідомлення оператора у розмову. Відвідувач бачить його миттєво (як відповідь живого оператора) — і у нашому віджеті, і через `GET /v1/api/messages` у вашому власному чаті.\n\nЯкщо у бота ввімкнено автопереклад, текст перекладається мовою відвідувача перед доставкою.\n\n**Ідемпотентність.** Додайте заголовок `Idempotency-Key` — і повтор після таймауту не виконає дію вдруге: повернеться збережена відповідь першої спроби з `Idempotency-Replayed: true` (ключ живе 24 години).",
        "operationId": "operatorReply",
        "parameters": [
          {
            "name": "sid",
            "in": "path",
            "required": true,
            "schema": {
              "$ref": "#/components/schemas/SessionID"
            },
            "description": "Ідентифікатор розмови (`session_id`).",
            "example": "3f6c1b9e-7f42-4d2a-9a6f-0d5b1f0b21c7"
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "text"
                ],
                "properties": {
                  "text": {
                    "type": "string",
                    "minLength": 1,
                    "description": "Текст відповіді оператора. Порожній рядок → 400."
                  }
                }
              },
              "examples": {
                "reply": {
                  "value": {
                    "text": "Вітаю! Так, доставимо завтра до 14:00."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Надіслано.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "status"
                  ],
                  "properties": {
                    "status": {
                      "type": "string",
                      "const": "ok"
                    }
                  }
                },
                "examples": {
                  "sent": {
                    "value": {
                      "status": "ok"
                    }
                  }
                }
              }
            },
            "headers": {
              "Idempotency-Replayed": {
                "schema": {
                  "type": "string",
                  "enum": [
                    "true"
                  ]
                },
                "description": "Присутній лише коли це відтворена відповідь попереднього запиту з тим самим `Idempotency-Key` (дія НЕ виконувалася повторно)."
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "Звернення не знайдено або належить іншому боту.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "default": {
                    "value": {
                      "error": "thread not found",
                      "code": "not_found"
                    }
                  }
                }
              }
            }
          },
          "409": {
            "$ref": "#/components/responses/IdempotencyConflict"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/v1/api/operator/threads/{sid}/resolve": {
      "post": {
        "tags": [
          "Operator"
        ],
        "summary": "Завершити звернення",
        "description": "Завершує звернення до оператора — далі відвідувачу знову відповідає бот. Статус розмови стає `resolved`. Тіло запиту не потрібне.",
        "operationId": "operatorResolve",
        "parameters": [
          {
            "name": "sid",
            "in": "path",
            "required": true,
            "schema": {
              "$ref": "#/components/schemas/SessionID"
            },
            "description": "Ідентифікатор розмови (`session_id`).",
            "example": "3f6c1b9e-7f42-4d2a-9a6f-0d5b1f0b21c7"
          }
        ],
        "responses": {
          "200": {
            "description": "Завершено.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "status"
                  ],
                  "properties": {
                    "status": {
                      "type": "string",
                      "const": "resolved"
                    }
                  }
                },
                "examples": {
                  "resolved": {
                    "value": {
                      "status": "resolved"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "Звернення не знайдено або належить іншому боту.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "default": {
                    "value": {
                      "error": "thread not found",
                      "code": "not_found"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/v1/api/operator/threads/{sid}/typing": {
      "post": {
        "tags": [
          "Operator"
        ],
        "summary": "Оператор друкує",
        "description": "Дзеркальний сигнал у інший бік: відвідувач бачить «оператор друкує…» у віджеті, а ваш власний чат отримує `operator_typing: true` у `GET /v1/api/messages`. Ті самі правила частоти (раз на 3 с) і згасання (~6 с). Тіло запиту не потрібне; превʼю тексту оператора не передається.",
        "operationId": "operatorTyping",
        "parameters": [
          {
            "name": "sid",
            "in": "path",
            "required": true,
            "schema": {
              "$ref": "#/components/schemas/SessionID"
            },
            "description": "Ідентифікатор розмови (`session_id`).",
            "example": "3f6c1b9e-7f42-4d2a-9a6f-0d5b1f0b21c7"
          }
        ],
        "responses": {
          "204": {
            "description": "Прийнято. Тіла немає."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "Тред не знайдено або належить іншому боту.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "default": {
                    "value": {
                      "error": "thread not found",
                      "code": "not_found"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/api/typing": {
      "post": {
        "tags": [
          "Handoff"
        ],
        "summary": "Клієнт друкує",
        "description": "Сигнал, що відвідувач зараз набирає повідомлення. Оператор бачить «клієнт друкує…» у кабінеті (або у вашому операторському UI — через `typing`/`drafts` у `GET /v1/api/operator/threads`).\n\nНадсилайте не частіше ніж раз на 3 секунди, поки клієнт друкує; сигнал згасає сам приблизно через 6 секунд.\n\nНеобовʼязкове поле `text` передає **чернетку** — те, що клієнт друкує просто зараз, ще не надіславши. Оператор бачить її наживо і встигає підготувати відповідь. Не надсилайте `text`, якщо не хочете показувати недонабране (це видимість особистого набору тексту — вмикайте усвідомлено). Чернетка обрізається до 500 символів.\n\nЕндпоїнт мовчазний: якщо сесія належить іншому боту, у відповідь усе одно `204`, але сигнал ігнорується.",
        "operationId": "visitorTyping",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "session_id"
                ],
                "properties": {
                  "session_id": {
                    "$ref": "#/components/schemas/SessionID"
                  },
                  "text": {
                    "type": "string",
                    "maxLength": 500,
                    "description": "Чернетка: текст, який клієнт набирає просто зараз. Опційно. Довше за 500 символів обрізається. Порожній рядок або відсутнє поле → лише індикатор «друкує…», без превʼю."
                  }
                }
              },
              "examples": {
                "signalOnly": {
                  "summary": "Лише індикатор",
                  "value": {
                    "session_id": "3f6c1b9e-7f42-4d2a-9a6f-0d5b1f0b21c7"
                  }
                },
                "withDraft": {
                  "summary": "З превʼю чернетки",
                  "value": {
                    "session_id": "3f6c1b9e-7f42-4d2a-9a6f-0d5b1f0b21c7",
                    "text": "чи можна замовити на суб"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "204": {
            "description": "Прийнято. Тіла немає."
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/api/visitors/erase": {
      "post": {
        "tags": [
          "Chat"
        ],
        "summary": "Видалити дані відвідувача",
        "description": "Стирає все, що бот зберіг про одного відвідувача: переписку, звернення до оператора та довготривалу памʼять. Це відповідь на вимогу людини «видаліть усе, що ви про мене маєте».\n\nВкажіть `session_id` (конкретна розмова) або `user_token` (той самий ідентифікатор, який ви передаєте у віджет для впізнавання клієнта) — або обидва. За `user_token` стираються дані з усіх його розмов із цим ботом.\n\nДіє лише в межах бота, якому належить ключ.\n\nОперація незворотна. У відповідь `200` лише тоді, коли дані справді видалено: якщо стирання не вдалося, ви отримаєте помилку, а не бадьоре «готово».",
        "operationId": "eraseVisitor",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "description": "Потрібне щонайменше одне з полів.",
                "properties": {
                  "session_id": {
                    "$ref": "#/components/schemas/SessionID"
                  },
                  "user_token": {
                    "type": "string",
                    "maxLength": 256,
                    "description": "Ваш ідентифікатор клієнта, переданий віджету для впізнавання.",
                    "example": "user-4821"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Дані видалено.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "example": "erased"
                    }
                  }
                }
              }
            },
            "headers": {
              "Idempotency-Replayed": {
                "schema": {
                  "type": "string",
                  "enum": [
                    "true"
                  ]
                },
                "description": "Присутній лише коли це відтворена відповідь попереднього запиту з тим самим `Idempotency-Key` (видалення НЕ виконувалося повторно)."
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "Видалення даних відвідувача для цього агента недоступне.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "default": {
                    "value": {
                      "error": "erasure unavailable",
                      "code": "not_found"
                    }
                  }
                }
              }
            }
          },
          "409": {
            "$ref": "#/components/responses/IdempotencyConflict"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "Видалити не вдалося — дані лишились на місці. Повторіть запит."
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ]
      }
    },
    "/v1/auth/login": {
      "post": {
        "tags": [
          "Автентифікація"
        ],
        "summary": "Увійти",
        "description": "Відкриває сесію. Кука ставиться на реєстрований домен, тож діє й у кабінеті, і на лендингу, і в посібнику.",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "email": {
                    "type": "string",
                    "format": "email"
                  },
                  "password": {
                    "type": "string"
                  }
                },
                "required": [
                  "email",
                  "password"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Сесію відкрито.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "user_id": {
                      "type": "string"
                    },
                    "email": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Пара не збіглася. Відповідь однакова і для невідомої адреси, і для хибного пароля.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Забагато спроб.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/auth/logout": {
      "post": {
        "tags": [
          "Автентифікація"
        ],
        "summary": "Вийти",
        "description": "Гасить поточну сесію.",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "responses": {
          "200": {
            "description": "Сесію закрито."
          },
          "401": {
            "description": "Сесії не було.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Роль поточного користувача не дозволяє цю дію.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/auth/logout-all": {
      "post": {
        "tags": [
          "Автентифікація"
        ],
        "summary": "Вийти на всіх пристроях",
        "description": "Гасить УСІ сесії акаунта. Те, чим користуються після витоку пароля.",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "responses": {
          "200": {
            "description": "Усі сесії закрито."
          },
          "401": {
            "description": "Немає сесії.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Роль поточного користувача не дозволяє цю дію.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/auth/me": {
      "get": {
        "tags": [
          "Автентифікація"
        ],
        "summary": "Хто я",
        "description": "Поточний користувач і його роль. Кабінет кличе це першим, щоб вирішити, що показувати.",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "responses": {
          "200": {
            "description": "Профіль.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "user_id": {
                      "type": "string"
                    },
                    "email": {
                      "type": "string"
                    },
                    "role": {
                      "type": "string"
                    },
                    "workspace_id": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Сесія відсутня або згасла.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Роль поточного користувача не дозволяє цю дію.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/auth/password": {
      "post": {
        "tags": [
          "Автентифікація"
        ],
        "summary": "Змінити пароль",
        "description": "Вимагає чинний пароль. Успіх гасить решту сесій.",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "old_password": {
                    "type": "string"
                  },
                  "new_password": {
                    "type": "string",
                    "minLength": 8
                  }
                },
                "required": [
                  "old_password",
                  "new_password"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Пароль змінено."
          },
          "401": {
            "description": "Чинний пароль не збігся.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Роль поточного користувача не дозволяє цю дію.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/auth/signup": {
      "post": {
        "tags": [
          "Автентифікація"
        ],
        "summary": "Створити акаунт",
        "description": "Реєструє робочий простір і одразу відкриває сесію.",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "email": {
                    "type": "string",
                    "format": "email"
                  },
                  "password": {
                    "type": "string",
                    "minLength": 8
                  }
                },
                "required": [
                  "email",
                  "password"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Акаунт створено, куку сесії встановлено.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "user_id": {
                      "type": "string"
                    },
                    "email": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Адреса зайнята або пароль закороткий.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Забагато спроб з цієї адреси.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/auth/sso": {
      "get": {
        "tags": [
          "Автентифікація"
        ],
        "summary": "Вхід через зовнішнього постачальника",
        "description": "Починає SSO-обмін і повертає браузер у кабінет.",
        "security": [],
        "responses": {
          "302": {
            "description": "Перенаправлення до постачальника."
          }
        }
      }
    },
    "/v1/billing/callback": {
      "post": {
        "tags": [
          "Тарифікація"
        ],
        "summary": "Зворотний виклик платіжного шлюзу",
        "description": "Кличе шлюз, не ви. Тіло — форма з підписом; невідоме замовлення отримує 2xx, бо повторна доставка його відомим не зробить.",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": {
                "type": "object",
                "properties": {
                  "data": {
                    "type": "string"
                  },
                  "signature": {
                    "type": "string"
                  }
                },
                "required": [
                  "data",
                  "signature"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Прийнято."
          }
        }
      }
    },
    "/v1/billing/cancel": {
      "post": {
        "tags": [
          "Тарифікація"
        ],
        "summary": "Скасувати підписку",
        "description": "Підписка доживає оплачений період і не поновлюється.",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "responses": {
          "200": {
            "description": "Скасовано."
          },
          "401": {
            "description": "Немає дійсної сесії — увійдіть заново.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Роль поточного користувача не дозволяє цю дію.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/billing/checkout": {
      "post": {
        "tags": [
          "Тарифікація"
        ],
        "summary": "Оформити підписку",
        "description": "Повертає дані форми платіжного шлюзу.",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "plan_id": {
                    "type": "string"
                  }
                },
                "required": [
                  "plan_id"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Дані для переходу на оплату.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "string"
                    },
                    "signature": {
                      "type": "string"
                    },
                    "action": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Немає дійсної сесії — увійдіть заново.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Роль поточного користувача не дозволяє цю дію.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/billing/credit-checkout": {
      "post": {
        "tags": [
          "Тарифікація"
        ],
        "summary": "Купити пакет кредитів",
        "description": "Разова оплата; кредити лягають понад місячний запас.",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "pack_id": {
                    "type": "string"
                  }
                },
                "required": [
                  "pack_id"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Дані для переходу на оплату.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "string"
                    },
                    "signature": {
                      "type": "string"
                    },
                    "action": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Немає дійсної сесії — увійдіть заново.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Роль поточного користувача не дозволяє цю дію.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/billing/credit-packs": {
      "get": {
        "tags": [
          "Тарифікація"
        ],
        "summary": "Пакети кредитів",
        "description": "Разові пакети понад місячний запас тарифу.",
        "security": [],
        "responses": {
          "200": {
            "description": "Пакети.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "packs": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "credits": {
                            "type": "integer"
                          },
                          "price": {
                            "type": "number"
                          },
                          "currency": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/billing/plans": {
      "get": {
        "tags": [
          "Тарифікація"
        ],
        "summary": "Список тарифів",
        "description": "Доступні тарифи з місячним запасом кредитів.",
        "security": [],
        "responses": {
          "200": {
            "description": "Тарифи.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "plans": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "title": {
                            "type": "string"
                          },
                          "price": {
                            "type": "number"
                          },
                          "currency": {
                            "type": "string",
                            "example": "UAH"
                          },
                          "monthly_credits": {
                            "type": "integer"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/billing/sync": {
      "post": {
        "tags": [
          "Тарифікація"
        ],
        "summary": "Звірити оплату",
        "description": "Тягне статус із платіжного шлюзу вручну — на випадок, якщо зворотний виклик не дійшов.",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "responses": {
          "200": {
            "description": "Стан оновлено."
          },
          "401": {
            "description": "Немає дійсної сесії — увійдіть заново.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Роль поточного користувача не дозволяє цю дію.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/dashboard": {
      "get": {
        "tags": [
          "Кабінет · Агенти"
        ],
        "summary": "Огляд робочого простору",
        "description": "Агенти, залишок кредитів, тариф і нещодавно видалені агенти (`deleted_bots`) — окремої ручки для кошика немає.",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "responses": {
          "200": {
            "description": "Огляд.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "bots": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "name": {
                            "type": "string"
                          },
                          "created_at": {
                            "type": "string",
                            "format": "date-time"
                          }
                        }
                      }
                    },
                    "deleted_bots": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "name": {
                            "type": "string"
                          },
                          "purge_at": {
                            "type": "string",
                            "format": "date-time"
                          }
                        }
                      }
                    },
                    "plan": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "title": {
                          "type": "string"
                        }
                      }
                    },
                    "credits": {
                      "type": "object",
                      "properties": {
                        "left": {
                          "type": "number"
                        },
                        "monthly": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Немає дійсної сесії.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Роль не дозволяє цю дію.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Агента не знайдено або він належить іншому робочому простору.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/dashboard/agent-presets": {
      "get": {
        "tags": [
          "Кабінет · Хід агента"
        ],
        "summary": "Каталог пресетів",
        "description": "Пресет — це оверлей на хід, а не набір значень у формі. Кілька пресетів **схрещуються** на одному агенті.",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "responses": {
          "200": {
            "description": "Пресети.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "presets": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/AgentPreset"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Немає дійсної сесії.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Роль не дозволяє цю дію.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Агента не знайдено або він належить іншому робочому простору.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/dashboard/bots": {
      "post": {
        "tags": [
          "Кабінет · Агенти"
        ],
        "summary": "Створити агента",
        "description": "Новий агент із дефолтним ходом. Пайплайн налаштовувати не обовʼязково — агент робочий одразу.",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string"
                  }
                },
                "required": [
                  "name"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Створено.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "name": {
                      "type": "string"
                    },
                    "public_key": {
                      "type": "string",
                      "description": "Ключ `pk_` для віджета."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Немає дійсної сесії.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Роль не дозволяє цю дію.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Агента не знайдено або він належить іншому робочому простору.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/dashboard/bots/{id}": {
      "patch": {
        "tags": [
          "Кабінет · Агенти"
        ],
        "summary": "Перейменувати агента",
        "description": "Змінює лише назву.",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Ідентифікатор агента."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string"
                  }
                },
                "required": [
                  "name"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Оновлено."
          },
          "401": {
            "description": "Немає дійсної сесії.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Роль не дозволяє цю дію.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Агента не знайдено або він належить іншому робочому простору.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Кабінет · Агенти"
        ],
        "summary": "Видалити агента",
        "description": "Мʼяке видалення: агент зникає зі списку й перестає відповідати, але його можна повернути до дати остаточного стирання. Вона повертається в `purge_at`.",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Ідентифікатор агента."
          }
        ],
        "responses": {
          "200": {
            "description": "Видалено.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "purge_at": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Немає дійсної сесії.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Роль не дозволяє цю дію.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Агента не знайдено або він належить іншому робочому простору.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/dashboard/bots/{id}/agent-presets/{preset}": {
      "post": {
        "tags": [
          "Кабінет · Хід агента"
        ],
        "summary": "Накласти пресет",
        "description": "Накладається на ЧЕРНЕТКУ, не на бойову версію. Якщо крок із таким імʼям уже є, це конфлікт для людини, а не мовчазне перетирання.",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Ідентифікатор агента."
          },
          {
            "name": "preset",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Накладено.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "draft": {
                      "$ref": "#/components/schemas/AgentVersion"
                    },
                    "warnings": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "installed": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Немає дійсної сесії.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Роль не дозволяє цю дію.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Агента не знайдено або він належить іншому робочому простору.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Конфлікт із наявним кроком.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "conflicts": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "step_id": {
                            "type": "string"
                          },
                          "existing_origin": {
                            "type": "string"
                          },
                          "incoming_origin": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Кабінет · Хід агента"
        ],
        "summary": "Зняти пресет",
        "description": "Прибирає лише кроки цього пресета: походження живе в самому кроці, тож ручні кроки лишаються на місці.",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Ідентифікатор агента."
          },
          {
            "name": "preset",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Знято.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "draft": {
                      "$ref": "#/components/schemas/AgentVersion"
                    },
                    "warnings": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Немає дійсної сесії.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Роль не дозволяє цю дію.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Агента не знайдено або він належить іншому робочому простору.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/dashboard/bots/{id}/config": {
      "get": {
        "tags": [
          "Кабінет · Агенти"
        ],
        "summary": "Налаштування агента",
        "description": "Поведінка, тон, вітання, обмеження — усе, що не є ходом.",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Ідентифікатор агента."
          }
        ],
        "responses": {
          "200": {
            "description": "Налаштування.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": true
                }
              }
            }
          },
          "401": {
            "description": "Немає дійсної сесії.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Роль не дозволяє цю дію.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Агента не знайдено або він належить іншому робочому простору.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "put": {
        "tags": [
          "Кабінет · Агенти"
        ],
        "summary": "Зберегти налаштування",
        "description": "Зміна діє одразу: це не версіонований ресурс, на відміну від ходу.",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Ідентифікатор агента."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": true
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Збережено."
          },
          "401": {
            "description": "Немає дійсної сесії.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Роль не дозволяє цю дію.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Агента не знайдено або він належить іншому робочому простору.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/dashboard/bots/{id}/dev-connection": {
      "get": {
        "tags": [
          "Кабінет · Підключення"
        ],
        "summary": "Зовнішній розробник",
        "description": "Ключ акаунта не повертається ніколи — приходить лише підказка, за якою власник упізнає підключений ключ.",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Ідентифікатор агента."
          }
        ],
        "responses": {
          "200": {
            "description": "Підключення.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "key_hint": {
                      "type": "string"
                    },
                    "max_concurrent": {
                      "type": "integer"
                    },
                    "max_per_day": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Немає дійсної сесії.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Роль не дозволяє цю дію.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Агента не знайдено або він належить іншому робочому простору.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "put": {
        "tags": [
          "Кабінет · Підключення"
        ],
        "summary": "Зберегти підключення",
        "description": "Поле `api_key` передавайте ЛИШЕ коли справді вводите ключ: відсутнє поле означає «не чіпали», порожній рядок — «відключити». Порожнє значення, надіслане за звичкою разом із лімітами, мовчки вимкнуло б робоче підключення.",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Ідентифікатор агента."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "max_concurrent": {
                    "type": "integer"
                  },
                  "max_per_day": {
                    "type": "integer"
                  },
                  "api_key": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Збережено."
          },
          "401": {
            "description": "Немає дійсної сесії.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Роль не дозволяє цю дію.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Агента не знайдено або він належить іншому робочому простору.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/dashboard/bots/{id}/erase-visitor": {
      "post": {
        "tags": [
          "Кабінет · Агенти"
        ],
        "summary": "Стерти дані відвідувача",
        "description": "Прибирає памʼять і розмови конкретного відвідувача — виконання права на забуття з боку кабінету.",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Ідентифікатор агента."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "session_id": {
                    "type": "string"
                  }
                },
                "required": [
                  "session_id"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Стерто."
          },
          "400": {
            "description": "Тіло не є коректним JSON або немає `session_id`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Немає дійсної сесії.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Роль не дозволяє цю дію.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Агента не знайдено або він належить іншому робочому простору.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/dashboard/bots/{id}/events": {
      "get": {
        "tags": [
          "Кабінет · Агенти"
        ],
        "summary": "Потік подій агента (SSE)",
        "description": "Довгоживучий потік `text/event-stream`: нові звернення, зміни статусу, завершені запуски. Кабінет тримає його відкритим замість опитування.",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Ідентифікатор агента."
          }
        ],
        "responses": {
          "200": {
            "description": "Потік подій."
          },
          "401": {
            "description": "Немає дійсної сесії.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Роль не дозволяє цю дію.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Агента не знайдено або він належить іншому робочому простору.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/dashboard/bots/{id}/faq/suggest": {
      "post": {
        "tags": [
          "Кабінет · Агенти"
        ],
        "summary": "Запропонувати часті питання",
        "description": "Складає чернетку FAQ із того, що в агента справді питали. Нічого не зберігає — рішення за вами.",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Ідентифікатор агента."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Чернетка.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "question": {
                            "type": "string"
                          },
                          "answer": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Немає дійсної сесії.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Роль не дозволяє цю дію.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Агента не знайдено або він належить іншому робочому простору.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/dashboard/bots/{id}/feedback": {
      "get": {
        "tags": [
          "Кабінет · Агенти"
        ],
        "summary": "Оцінки відповідей",
        "description": "Скільки разів відвідувачі схвалили відповідь агента, а скільки — ні.",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Ідентифікатор агента."
          }
        ],
        "responses": {
          "200": {
            "description": "Підсумок.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "up": {
                      "type": "integer"
                    },
                    "down": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Немає дійсної сесії.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Роль не дозволяє цю дію.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Агента не знайдено або він належить іншому робочому простору.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/dashboard/bots/{id}/files": {
      "post": {
        "tags": [
          "Кабінет · Знання"
        ],
        "summary": "Завантажити документ",
        "description": "PDF, DOCX, TXT або MD стають джерелом знань.",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Ідентифікатор агента."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary"
                  }
                },
                "required": [
                  "file"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Додано.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "source_id": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Немає дійсної сесії.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Роль не дозволяє цю дію.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Агента не знайдено або він належить іншому робочому простору.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "Файл завеликий.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/dashboard/bots/{id}/identity-secret": {
      "post": {
        "tags": [
          "Кабінет · Агенти"
        ],
        "summary": "Перевипустити ключ підтвердження особи",
        "description": "Ним ваш сервер підписує підтвердження, що відвідувач — саме той, за кого себе видає. Повертається лише в момент випуску.",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Ідентифікатор агента."
          }
        ],
        "responses": {
          "200": {
            "description": "Новий ключ.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "identity_secret": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Немає дійсної сесії.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Роль не дозволяє цю дію.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Агента не знайдено або він належить іншому робочому простору.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/dashboard/bots/{id}/leads": {
      "get": {
        "tags": [
          "Кабінет · Агенти"
        ],
        "summary": "Залишені контакти",
        "description": "Те, що відвідувачі лишили агенту для звʼязку.",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Ідентифікатор агента."
          }
        ],
        "responses": {
          "200": {
            "description": "Контакти.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "leads": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Lead"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Немає дійсної сесії.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Роль не дозволяє цю дію.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Агента не знайдено або він належить іншому робочому простору.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/dashboard/bots/{id}/media": {
      "get": {
        "tags": [
          "Кабінет · Знання"
        ],
        "summary": "Медіа агента",
        "description": "Картинки, які агент може показувати у відповідях.",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Ідентифікатор агента."
          }
        ],
        "responses": {
          "200": {
            "description": "Медіа.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "media": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "url": {
                            "type": "string"
                          },
                          "description": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Немає дійсної сесії.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Роль не дозволяє цю дію.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Агента не знайдено або він належить іншому робочому простору.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Кабінет · Знання"
        ],
        "summary": "Завантажити медіа",
        "description": "Опис важливіший за файл: саме за ним агент вирішує, коли доречно показати зображення.",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Ідентифікатор агента."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary"
                  },
                  "description": {
                    "type": "string"
                  }
                },
                "required": [
                  "file"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Завантажено.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "url": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Немає дійсної сесії.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Роль не дозволяє цю дію.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Агента не знайдено або він належить іншому робочому простору.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/dashboard/bots/{id}/media/{mediaID}": {
      "delete": {
        "tags": [
          "Кабінет · Знання"
        ],
        "summary": "Видалити медіа",
        "description": "Прибирає файл і посилання на нього.",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Ідентифікатор агента."
          },
          {
            "name": "mediaID",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Видалено."
          },
          "401": {
            "description": "Немає дійсної сесії.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Роль не дозволяє цю дію.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Агента не знайдено або він належить іншому робочому простору.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/dashboard/bots/{id}/origins": {
      "post": {
        "tags": [
          "Кабінет · Агенти"
        ],
        "summary": "Дозволені домени віджета",
        "description": "Перелік сайтів, на яких дозволено запускати віджет цього агента. Порожньо = дозволено будь-де.",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Ідентифікатор агента."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "origins": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "example": "https://example.com"
                    }
                  }
                },
                "required": [
                  "origins"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Збережено."
          },
          "401": {
            "description": "Немає дійсної сесії.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Роль не дозволяє цю дію.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Агента не знайдено або він належить іншому робочому простору.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/dashboard/bots/{id}/pipeline": {
      "get": {
        "tags": [
          "Кабінет · Хід агента"
        ],
        "summary": "Хід агента",
        "description": "Повертає три речі: дефолтний хід платформи, опубліковану версію (`live`) і незбережену чернетку (`draft`). `live: null` означає, що агент працює на дефолті — це нормальний стан, а не помилка налаштування.",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Ідентифікатор агента."
          }
        ],
        "responses": {
          "200": {
            "description": "Хід.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "default": {
                      "$ref": "#/components/schemas/Pipeline"
                    },
                    "live": {
                      "$ref": "#/components/schemas/AgentVersion"
                    },
                    "draft": {
                      "$ref": "#/components/schemas/AgentVersion"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Немає дійсної сесії.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Роль не дозволяє цю дію.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Агента не знайдено або він належить іншому робочому простору.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "put": {
        "tags": [
          "Кабінет · Хід агента"
        ],
        "summary": "Зберегти чернетку",
        "description": "Пише в чернетку, не в бойову версію: агент продовжує працювати на `live`, доки не буде публікації. Незадоволені залежності кроків повертаються в `warnings` — це попередження, а не відмова.",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Ідентифікатор агента."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "pipeline": {
                    "$ref": "#/components/schemas/Pipeline"
                  }
                },
                "required": [
                  "pipeline"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Чернетку збережено.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "draft": {
                      "$ref": "#/components/schemas/AgentVersion"
                    },
                    "warnings": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Невідомий тип кроку, крок не в своїй фазі, дубль імені або `model_call` без `prompt_assemble`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Немає дійсної сесії.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Роль не дозволяє цю дію.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Агента не знайдено або він належить іншому робочому простору.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Кабінет · Хід агента"
        ],
        "summary": "Відкинути чернетку",
        "description": "Повертає редактор до опублікованої версії.",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Ідентифікатор агента."
          }
        ],
        "responses": {
          "200": {
            "description": "Чернетку прибрано."
          },
          "401": {
            "description": "Немає дійсної сесії.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Роль не дозволяє цю дію.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Агента не знайдено або він належить іншому робочому простору.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/dashboard/bots/{id}/pipeline/publish": {
      "post": {
        "tags": [
          "Кабінет · Хід агента"
        ],
        "summary": "Опублікувати чернетку",
        "description": "Чернетка стає бойовою, попередня версія йде в архів. У відповіді — дифф, який варто показати людині ДО того, як зміна поїде на живих відвідувачів.",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Ідентифікатор агента."
          }
        ],
        "responses": {
          "200": {
            "description": "Опубліковано.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "live": {
                      "$ref": "#/components/schemas/AgentVersion"
                    },
                    "diff": {
                      "$ref": "#/components/schemas/PipelineDiff"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Немає дійсної сесії.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Роль не дозволяє цю дію.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Агента не знайдено або він належить іншому робочому простору.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Публікувати нічого: чернетки немає.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/dashboard/bots/{id}/pipeline/versions": {
      "get": {
        "tags": [
          "Кабінет · Хід агента"
        ],
        "summary": "Історія версій",
        "description": "Опубліковані версії від найновішої.",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Ідентифікатор агента."
          }
        ],
        "responses": {
          "200": {
            "description": "Версії.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "versions": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/AgentVersion"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Немає дійсної сесії.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Роль не дозволяє цю дію.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Агента не знайдено або він належить іншому робочому простору.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/dashboard/bots/{id}/preview-identity": {
      "post": {
        "tags": [
          "Кабінет · Агенти"
        ],
        "summary": "Перевірити підпис особи",
        "description": "Приймає підписане підтвердження й показує, як його прочитає агент. Потрібне, щоб налагодити інтеграцію, не ганяючи живих відвідувачів.",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Ідентифікатор агента."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "payload": {
                    "type": "string"
                  },
                  "signature": {
                    "type": "string"
                  }
                },
                "required": [
                  "payload",
                  "signature"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Розібраний підпис.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "valid": {
                      "type": "boolean"
                    },
                    "user": {
                      "type": "object",
                      "additionalProperties": true
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Немає дійсної сесії.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Роль не дозволяє цю дію.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Агента не знайдено або він належить іншому робочому простору.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/dashboard/bots/{id}/restore": {
      "post": {
        "tags": [
          "Кабінет · Агенти"
        ],
        "summary": "Повернути видаленого агента",
        "description": "Працює до дати остаточного стирання; після неї даних уже немає.",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Ідентифікатор агента."
          }
        ],
        "responses": {
          "200": {
            "description": "Повернуто."
          },
          "401": {
            "description": "Немає дійсної сесії.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Роль не дозволяє цю дію.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Агента не знайдено або він належить іншому робочому простору.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/dashboard/bots/{id}/runs": {
      "get": {
        "tags": [
          "Кабінет · Тригери"
        ],
        "summary": "Журнал запусків",
        "description": "Кожен хід агента з вартістю в кредитах і причиною збою. Це те, чим відповідають на питання «чому агент так зробив».",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Ідентифікатор агента."
          }
        ],
        "responses": {
          "200": {
            "description": "Запуски.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "runs": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Run"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Немає дійсної сесії.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Роль не дозволяє цю дію.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Агента не знайдено або він належить іншому робочому простору.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/dashboard/bots/{id}/scrape": {
      "get": {
        "tags": [
          "Кабінет · Знання"
        ],
        "summary": "Стан обходу сайту",
        "description": "Опитування фонової задачі.",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Ідентифікатор агента."
          }
        ],
        "responses": {
          "200": {
            "description": "Стан.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "enum": [
                        "idle",
                        "scanning",
                        "done",
                        "error"
                      ]
                    },
                    "pages": {
                      "type": "integer"
                    },
                    "error": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Немає дійсної сесії.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Роль не дозволяє цю дію.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Агента не знайдено або він належить іншому робочому простору.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Кабінет · Знання"
        ],
        "summary": "Обійти сайт",
        "description": "Знімає сторінки й перетворює їх на джерела знань. Працює у фоні — стан опитуйте тим самим шляхом через `GET`.",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Ідентифікатор агента."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "url": {
                    "type": "string",
                    "format": "uri"
                  },
                  "max_pages": {
                    "type": "integer"
                  }
                },
                "required": [
                  "url"
                ]
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Обхід почато."
          },
          "401": {
            "description": "Немає дійсної сесії.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Роль не дозволяє цю дію.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Агента не знайдено або він належить іншому робочому простору.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/dashboard/bots/{id}/secret": {
      "post": {
        "tags": [
          "Кабінет · Агенти"
        ],
        "summary": "Перевипустити серверний ключ",
        "description": "Видає новий ключ `sk_` для викликів сервер-до-сервера. **Повний ключ повертається рівно один раз** — далі система знає лише його відбиток. Старий ключ перестає діяти негайно.",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Ідентифікатор агента."
          }
        ],
        "responses": {
          "200": {
            "description": "Новий ключ.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "secret_key": {
                      "type": "string",
                      "example": "sk_live_…"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Немає дійсної сесії.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Роль не дозволяє цю дію.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Агента не знайдено або він належить іншому робочому простору.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/dashboard/bots/{id}/skills": {
      "get": {
        "tags": [
          "Кабінет · Дії"
        ],
        "summary": "Дії агента",
        "description": "HTTP-виклики, які агент може зробити сам.",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Ідентифікатор агента."
          }
        ],
        "responses": {
          "200": {
            "description": "Дії.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "skills": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Skill"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Немає дійсної сесії.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Роль не дозволяє цю дію.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Агента не знайдено або він належить іншому робочому простору.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Кабінет · Дії"
        ],
        "summary": "Додати дію",
        "description": "Опис дії — це те, за чим модель вирішує, коли її кликати. Секрети йдуть у заголовки й у відповідях не показуються.",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Ідентифікатор агента."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Skill"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Додано."
          },
          "401": {
            "description": "Немає дійсної сесії.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Роль не дозволяє цю дію.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Агента не знайдено або він належить іншому робочому простору.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/dashboard/bots/{id}/skills/import": {
      "post": {
        "tags": [
          "Кабінет · Дії"
        ],
        "summary": "Імпортувати дії за посиланням",
        "description": "Читає OpenAPI за адресою й пропонує дії-кандидати.",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Ідентифікатор агента."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "url": {
                    "type": "string",
                    "format": "uri"
                  }
                },
                "required": [
                  "url"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Кандидати.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "skills": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Skill"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Немає дійсної сесії.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Роль не дозволяє цю дію.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Агента не знайдено або він належить іншому робочому простору.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/dashboard/bots/{id}/skills/import-file": {
      "post": {
        "tags": [
          "Кабінет · Дії"
        ],
        "summary": "Імпортувати дії з файлів документації",
        "description": "Приймає файли специфікації. `base_url` потрібен, якщо документація описує лише шляхи без хоста.",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Ідентифікатор агента."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "properties": {
                  "file": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "format": "binary"
                    }
                  },
                  "base_url": {
                    "type": "string"
                  }
                },
                "required": [
                  "file"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Кандидати.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "skills": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Skill"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Немає дійсної сесії.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Роль не дозволяє цю дію.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Агента не знайдено або він належить іншому робочому простору.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/dashboard/bots/{id}/skills/presets": {
      "get": {
        "tags": [
          "Кабінет · Дії"
        ],
        "summary": "Готові набори дій",
        "description": "Інтеграції, які не треба описувати руками.",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Ідентифікатор агента."
          }
        ],
        "responses": {
          "200": {
            "description": "Набори.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "presets": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "title": {
                            "type": "string"
                          },
                          "inputs": {
                            "type": "array",
                            "items": {
                              "type": "object",
                              "properties": {
                                "name": {
                                  "type": "string"
                                },
                                "title": {
                                  "type": "string"
                                },
                                "secret": {
                                  "type": "boolean"
                                }
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Немає дійсної сесії.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Роль не дозволяє цю дію.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Агента не знайдено або він належить іншому робочому простору.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Кабінет · Дії"
        ],
        "summary": "Підключити набір дій",
        "description": "`inputs` — те, чого набір потребує: ключі, ідентифікатори. Секретні значення в подальших відповідях не повертаються.",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Ідентифікатор агента."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "preset_id": {
                    "type": "string"
                  },
                  "inputs": {
                    "type": "object",
                    "additionalProperties": {
                      "type": "string"
                    }
                  }
                },
                "required": [
                  "preset_id"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Підключено."
          },
          "401": {
            "description": "Немає дійсної сесії.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Роль не дозволяє цю дію.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Агента не знайдено або він належить іншому робочому простору.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/dashboard/bots/{id}/skills/presets/{preset}/sync": {
      "post": {
        "tags": [
          "Кабінет · Дії"
        ],
        "summary": "Оновити набір дій",
        "description": "Підтягує зміни набору, зберігаючи введені раніше значення.",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Ідентифікатор агента."
          },
          {
            "name": "preset",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Оновлено."
          },
          "401": {
            "description": "Немає дійсної сесії.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Роль не дозволяє цю дію.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Агента не знайдено або він належить іншому робочому простору.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/dashboard/bots/{id}/skills/{skillID}": {
      "patch": {
        "tags": [
          "Кабінет · Дії"
        ],
        "summary": "Змінити доступ до дії",
        "description": "`admin_only` лишає дію тільки для авторизованого власника сайту — анонімний відвідувач її не отримає.",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Ідентифікатор агента."
          },
          {
            "name": "skillID",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "admin_only": {
                    "type": "boolean"
                  }
                },
                "required": [
                  "admin_only"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Оновлено."
          },
          "401": {
            "description": "Немає дійсної сесії.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Роль не дозволяє цю дію.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Агента не знайдено або він належить іншому робочому простору.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Кабінет · Дії"
        ],
        "summary": "Видалити дію",
        "description": "Агент більше не бачить її у своєму наборі.",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Ідентифікатор агента."
          },
          {
            "name": "skillID",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Видалено."
          },
          "401": {
            "description": "Немає дійсної сесії.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Роль не дозволяє цю дію.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Агента не знайдено або він належить іншому робочому простору.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/dashboard/bots/{id}/skills/{skillID}/test": {
      "post": {
        "tags": [
          "Кабінет · Дії"
        ],
        "summary": "Перевірити дію",
        "description": "Виконує справжній виклик із заданими аргументами й показує сиру відповідь. Те, чим ловлять помилку в описі до того, як її знайде відвідувач.",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Ідентифікатор агента."
          },
          {
            "name": "skillID",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": true
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Результат виклику.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "integer"
                    },
                    "body": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Немає дійсної сесії.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Роль не дозволяє цю дію.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Агента не знайдено або він належить іншому робочому простору.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/dashboard/bots/{id}/source": {
      "get": {
        "tags": [
          "Кабінет · Знання"
        ],
        "summary": "Одне джерело",
        "description": "Повний текст джерела.",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Ідентифікатор агента."
          },
          {
            "name": "source_id",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Джерело.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Source"
                }
              }
            }
          },
          "401": {
            "description": "Немає дійсної сесії.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Роль не дозволяє цю дію.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Агента не знайдено або він належить іншому робочому простору.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/dashboard/bots/{id}/sources": {
      "get": {
        "tags": [
          "Кабінет · Знання"
        ],
        "summary": "Джерела знань",
        "description": "Усе, чого агента навчили.",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Ідентифікатор агента."
          }
        ],
        "responses": {
          "200": {
            "description": "Джерела.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "sources": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Source"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Немає дійсної сесії.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Роль не дозволяє цю дію.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Агента не знайдено або він належить іншому робочому простору.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Кабінет · Знання"
        ],
        "summary": "Додати текст",
        "description": "Текстове джерело власноруч.",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Ідентифікатор агента."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "title": {
                    "type": "string"
                  },
                  "text": {
                    "type": "string"
                  }
                },
                "required": [
                  "title",
                  "text"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Додано."
          },
          "401": {
            "description": "Немає дійсної сесії.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Роль не дозволяє цю дію.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Агента не знайдено або він належить іншому робочому простору.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "put": {
        "tags": [
          "Кабінет · Знання"
        ],
        "summary": "Змінити джерело",
        "description": "Переписує вміст; агент побачить нову редакцію після переіндексації.",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Ідентифікатор агента."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "source_id": {
                    "type": "string"
                  },
                  "title": {
                    "type": "string"
                  },
                  "text": {
                    "type": "string"
                  }
                },
                "required": [
                  "source_id"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Оновлено."
          },
          "401": {
            "description": "Немає дійсної сесії.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Роль не дозволяє цю дію.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Агента не знайдено або він належить іншому робочому простору.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Кабінет · Знання"
        ],
        "summary": "Видалити джерело",
        "description": "Ідентифікатор іде **параметром запиту**, а не в шляху: у сторінок, знятих із сайту, він є адресою, а скісні риски розірвали б шлях.",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Ідентифікатор агента."
          },
          {
            "name": "source_id",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Видалено."
          },
          "401": {
            "description": "Немає дійсної сесії.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Роль не дозволяє цю дію.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Агента не знайдено або він належить іншому робочому простору.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/dashboard/bots/{id}/state": {
      "get": {
        "tags": [
          "Кабінет · Тригери"
        ],
        "summary": "Памʼять агента",
        "description": "Ключ-значення, у якому агент тримає курсори й лічильники між запусками. `version` потрібна для запису.",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Ідентифікатор агента."
          }
        ],
        "responses": {
          "200": {
            "description": "Стан.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AgentState"
                }
              }
            }
          },
          "401": {
            "description": "Немає дійсної сесії.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Роль не дозволяє цю дію.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Агента не знайдено або він належить іншому робочому простору.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "put": {
        "tags": [
          "Кабінет · Тригери"
        ],
        "summary": "Записати памʼять агента",
        "description": "Порівняння-і-запис: передайте `version`, яку щойно прочитали. Якщо агент устиг змінити стан сам, запис відхиляється, а не затирає його роботу.",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Ідентифікатор агента."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "values": {
                    "type": "object",
                    "additionalProperties": {
                      "type": "string"
                    }
                  },
                  "version": {
                    "type": "integer"
                  }
                },
                "required": [
                  "values",
                  "version"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Записано.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "version": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Немає дійсної сесії.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Роль не дозволяє цю дію.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Агента не знайдено або він належить іншому робочому простору.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Версія застаріла — стан змінили паралельно. Прочитайте наново й повторіть.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/dashboard/bots/{id}/stats": {
      "get": {
        "tags": [
          "Кабінет · Агенти"
        ],
        "summary": "Статистика агента",
        "description": "Розмови й витрати по днях.",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Ідентифікатор агента."
          },
          {
            "name": "tz",
            "in": "query",
            "schema": {
              "type": "string",
              "example": "Europe/Kyiv"
            },
            "description": "Таймзона, у якій різати доби. Без неї — UTC."
          }
        ],
        "responses": {
          "200": {
            "description": "Статистика.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "days": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "date": {
                            "type": "string",
                            "format": "date"
                          },
                          "conversations": {
                            "type": "integer"
                          },
                          "credits": {
                            "type": "number"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Немає дійсної сесії.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Роль не дозволяє цю дію.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Агента не знайдено або він належить іншому робочому простору.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/dashboard/bots/{id}/telegram": {
      "get": {
        "tags": [
          "Кабінет · Підключення"
        ],
        "summary": "Telegram для операторів",
        "description": "Токен ніколи не повертається — лише ознака, що він заданий, і імʼя бота.",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Ідентифікатор агента."
          }
        ],
        "responses": {
          "200": {
            "description": "Налаштування.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "has_token": {
                      "type": "boolean"
                    },
                    "username": {
                      "type": "string"
                    },
                    "chat_ids": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Немає дійсної сесії.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Роль не дозволяє цю дію.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Агента не знайдено або він належить іншому робочому простору.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "put": {
        "tags": [
          "Кабінет · Підключення"
        ],
        "summary": "Зберегти Telegram",
        "description": "Порожній токен відключає інтеграцію.",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Ідентифікатор агента."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "token": {
                    "type": "string"
                  },
                  "chat_ids": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Збережено."
          },
          "401": {
            "description": "Немає дійсної сесії.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Роль не дозволяє цю дію.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Агента не знайдено або він належить іншому робочому простору.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/dashboard/bots/{id}/threads": {
      "get": {
        "tags": [
          "Кабінет · Звернення"
        ],
        "summary": "Черга звернень",
        "description": "Пошук іде по всій розмові на сервері, тож знаходить слово, сказане посеред діалогу, а не лише в останньому рядку.",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Ідентифікатор агента."
          },
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "active",
                "resolved"
              ]
            },
            "description": "Типово `active`."
          },
          {
            "name": "q",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Пошук по тексту розмов."
          },
          {
            "name": "cursor",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Курсор наступної сторінки."
          }
        ],
        "responses": {
          "200": {
            "description": "Звернення.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "threads": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Thread"
                      }
                    },
                    "next_cursor": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Немає дійсної сесії.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Роль не дозволяє цю дію.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Агента не знайдено або він належить іншому робочому простору.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/dashboard/bots/{id}/threads/{sid}": {
      "get": {
        "tags": [
          "Кабінет · Звернення"
        ],
        "summary": "Одне звернення",
        "description": "Повна історія розмови.",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Ідентифікатор агента."
          },
          {
            "name": "sid",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Ідентифікатор розмови."
          }
        ],
        "responses": {
          "200": {
            "description": "Розмова.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "messages": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Message"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Немає дійсної сесії.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Роль не дозволяє цю дію.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Агента не знайдено або він належить іншому робочому простору.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/dashboard/bots/{id}/threads/{sid}/claim": {
      "post": {
        "tags": [
          "Кабінет · Звернення"
        ],
        "summary": "Взяти або відпустити звернення",
        "description": "Показує решті команди, що розмовою вже хтось займається.",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Ідентифікатор агента."
          },
          {
            "name": "sid",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Ідентифікатор розмови."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "release": {
                    "type": "boolean",
                    "description": "`true` — відпустити."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Готово."
          },
          "401": {
            "description": "Немає дійсної сесії.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Роль не дозволяє цю дію.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Агента не знайдено або він належить іншому робочому простору.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/dashboard/bots/{id}/threads/{sid}/reply": {
      "post": {
        "tags": [
          "Кабінет · Звернення"
        ],
        "summary": "Відповісти від оператора",
        "description": "Поки розмову веде людина, агент навмисно мовчить.",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Ідентифікатор агента."
          },
          {
            "name": "sid",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Ідентифікатор розмови."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "text": {
                    "type": "string"
                  }
                },
                "required": [
                  "text"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Надіслано."
          },
          "401": {
            "description": "Немає дійсної сесії.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Роль не дозволяє цю дію.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Агента не знайдено або він належить іншому робочому простору.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/dashboard/bots/{id}/threads/{sid}/resolve": {
      "post": {
        "tags": [
          "Кабінет · Звернення"
        ],
        "summary": "Закрити звернення",
        "description": "Повертає розмову агенту.",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Ідентифікатор агента."
          },
          {
            "name": "sid",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Ідентифікатор розмови."
          }
        ],
        "responses": {
          "200": {
            "description": "Закрито."
          },
          "401": {
            "description": "Немає дійсної сесії.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Роль не дозволяє цю дію.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Агента не знайдено або він належить іншому робочому простору.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/dashboard/bots/{id}/threads/{sid}/typing": {
      "post": {
        "tags": [
          "Кабінет · Звернення"
        ],
        "summary": "Оператор друкує",
        "description": "Показує відвідувачу індикатор набору. Викликається часто, тож обмежений за частотою.",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Ідентифікатор агента."
          },
          {
            "name": "sid",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Ідентифікатор розмови."
          }
        ],
        "responses": {
          "204": {
            "description": "Прийнято."
          },
          "401": {
            "description": "Немає дійсної сесії.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Роль не дозволяє цю дію.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Агента не знайдено або він належить іншому робочому простору.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/dashboard/bots/{id}/triggers": {
      "get": {
        "tags": [
          "Кабінет · Тригери"
        ],
        "summary": "Тригери агента",
        "description": "Усе, що може розбудити агента.",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Ідентифікатор агента."
          }
        ],
        "responses": {
          "200": {
            "description": "Тригери.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "triggers": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Trigger"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Немає дійсної сесії.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Роль не дозволяє цю дію.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Агента не знайдено або він належить іншому робочому простору.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Кабінет · Тригери"
        ],
        "summary": "Створити тригер",
        "description": "Для `webhook` секрет підпису повертається **рівно тут і більше ніде**.",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Ідентифікатор агента."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Trigger"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Створено.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "trigger": {
                      "$ref": "#/components/schemas/Trigger"
                    },
                    "secret": {
                      "type": "string",
                      "description": "Лише для `webhook`, лише при створенні."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Немає дійсної сесії.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Роль не дозволяє цю дію.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Агента не знайдено або він належить іншому робочому простору.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/dashboard/bots/{id}/triggers/{tid}": {
      "put": {
        "tags": [
          "Кабінет · Тригери"
        ],
        "summary": "Змінити тригер",
        "description": "Зміна розкладу застосовується до наступного тику.",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Ідентифікатор агента."
          },
          {
            "name": "tid",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Trigger"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Оновлено.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "trigger": {
                      "$ref": "#/components/schemas/Trigger"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Немає дійсної сесії.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Роль не дозволяє цю дію.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Агента не знайдено або він належить іншому робочому простору.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Кабінет · Тригери"
        ],
        "summary": "Видалити тригер",
        "description": "Заплановані тики знімаються з черги.",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Ідентифікатор агента."
          },
          {
            "name": "tid",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Видалено."
          },
          "401": {
            "description": "Немає дійсної сесії.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Роль не дозволяє цю дію.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Агента не знайдено або він належить іншому робочому простору.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/dashboard/bots/{id}/triggers/{tid}/run": {
      "post": {
        "tags": [
          "Кабінет · Тригери"
        ],
        "summary": "Прогнати зараз",
        "description": "Тестовий запуск без чекання розкладу. Без нього розкладного агента неможливо перевірити, не дочекавшись ранку.",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Ідентифікатор агента."
          },
          {
            "name": "tid",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "input": {
                    "type": "string",
                    "description": "Чим замінити звичайну задачу тригера. Порожньо = взяти його власну."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Хід виконано.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "run_id": {
                      "type": "string"
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "done",
                        "failed"
                      ]
                    },
                    "answer": {
                      "type": "string"
                    },
                    "error": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Немає дійсної сесії.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Роль не дозволяє цю дію.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Агента не знайдено або він належить іншому робочому простору.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/dashboard/export": {
      "get": {
        "tags": [
          "Кабінет · Агенти"
        ],
        "summary": "Вивантажити дані робочого простору",
        "description": "Один архів з агентами, знаннями, розмовами й контактами — портативність даних.",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "responses": {
          "200": {
            "description": "Архів."
          },
          "401": {
            "description": "Немає дійсної сесії.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Роль не дозволяє цю дію.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Агента не знайдено або він належить іншому робочому простору.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/dashboard/invites": {
      "post": {
        "tags": [
          "Кабінет · Команда"
        ],
        "summary": "Запросити людину",
        "description": "`invite_url` приходить **єдиний раз**: система зберігає лише його відбиток. Загублене посилання не відновлюється — його відкликають і створюють нове.",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "email": {
                    "type": "string",
                    "format": "email"
                  },
                  "role": {
                    "type": "string",
                    "enum": [
                      "admin",
                      "operator"
                    ]
                  }
                },
                "required": [
                  "email",
                  "role"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Запрошення створено.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "invite": {
                      "$ref": "#/components/schemas/Invite"
                    },
                    "invite_url": {
                      "type": "string"
                    },
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Немає дійсної сесії.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Роль не дозволяє цю дію.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Агента не знайдено або він належить іншому робочому простору.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/dashboard/invites/{inviteID}": {
      "delete": {
        "tags": [
          "Кабінет · Команда"
        ],
        "summary": "Відкликати запрошення",
        "description": "Посилання перестає діяти.",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "inviteID",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Відкликано."
          },
          "401": {
            "description": "Немає дійсної сесії.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Роль не дозволяє цю дію.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Агента не знайдено або він належить іншому робочому простору.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/dashboard/join": {
      "post": {
        "tags": [
          "Кабінет · Команда"
        ],
        "summary": "Прийняти запрошення",
        "description": "Роль тут не передається: посилання і є дозволом, і діє лише для тієї адреси, на яку його виписали.",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "token": {
                    "type": "string"
                  }
                },
                "required": [
                  "token"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Приєднано.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "workspace_id": {
                      "type": "string"
                    },
                    "role": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Немає дійсної сесії.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Роль не дозволяє цю дію.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Агента не знайдено або він належить іншому робочому простору.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/dashboard/members": {
      "get": {
        "tags": [
          "Кабінет · Команда"
        ],
        "summary": "Учасники й запрошення",
        "description": "Склад робочого простору бачить лише власник.",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "responses": {
          "200": {
            "description": "Склад.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "members": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Member"
                      }
                    },
                    "invites": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Invite"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Немає дійсної сесії.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Роль не дозволяє цю дію.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Агента не знайдено або він належить іншому робочому простору.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/dashboard/members/{userID}": {
      "patch": {
        "tags": [
          "Кабінет · Команда"
        ],
        "summary": "Змінити роль",
        "description": "Роль вирішує рівно одне: що людина може змінювати.",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "userID",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "role": {
                    "type": "string",
                    "enum": [
                      "admin",
                      "operator"
                    ]
                  }
                },
                "required": [
                  "role"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Оновлено."
          },
          "401": {
            "description": "Немає дійсної сесії.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Роль не дозволяє цю дію.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Агента не знайдено або він належить іншому робочому простору.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Кабінет · Команда"
        ],
        "summary": "Прибрати учасника",
        "description": "Доступ зникає негайно, разом із сесіями.",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "userID",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Прибрано."
          },
          "401": {
            "description": "Немає дійсної сесії.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Роль не дозволяє цю дію.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Агента не знайдено або він належить іншому робочому простору.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/dashboard/pipeline/steps": {
      "get": {
        "tags": [
          "Кабінет · Хід агента"
        ],
        "summary": "Каталог типів кроків",
        "description": "Реєстр — джерело правди про те, які кроки існують. `Reads`/`Writes` дають редактору змогу не дати зберегти хід, у якому крок читає те, чого ще ніхто не поклав.",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "responses": {
          "200": {
            "description": "Каталог.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "steps": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/StepSpec"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Немає дійсної сесії.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Роль не дозволяє цю дію.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Агента не знайдено або він належить іншому робочому простору.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/hooks/{tid}": {
      "post": {
        "tags": [
          "Тригери"
        ],
        "summary": "Розбудити агента вебхуком",
        "operationId": "runWebhookTrigger",
        "security": [],
        "description": "Вхід вебхучного тригера: чужа система надсилає подію — агент прокидається й робить хід.\n\n**Звідки адреса й секрет.** У кабінеті створіть агенту тригер типу `webhook`. У відповідь ви отримаєте `hook_url` (саме цей шлях) і `secret`. **Секрет показується один раз** — збережіть його одразу; забули — створіть тригер наново.\n\n**Автентифікація — підпис, не ключ.** `X-Bot-Key` тут не використовується. Заголовок `X-Hook-Signature` містить `sha256=` + hex від `HMAC-SHA256(секрет, тіло запиту як є)`. Підписується **тільки тіло**, без timestamp — на відміну від наших вихідних вебхуків.\n\nСам `triggerID` непередбачуваний, але URL секретом не вважається: він осідає в чужих конфігураціях і логах. Тому без валідного підпису — `401`, завжди.\n\n```bash\nBODY='{\"order_id\":4417,\"status\":\"delayed\"}'\nSIG=$(printf '%s' \"$BODY\" | openssl dgst -sha256 -hmac \"$HOOK_SECRET\" -hex | awk '{print $2}')\n\ncurl -sS -X POST \"https://api.youselfbot.com/v1/hooks/trg_01j9x2h7q0\" \\\n  -H \"X-Hook-Signature: sha256=$SIG\" \\\n  -H \"Content-Type: application/json\" \\\n  -d \"$BODY\"\n```\n\n**Що бачить агент.** Вхід ходу — це задача, записана в тригері (поле `input`), плюс сире тіло вебхука під заголовком «Подія (тіло вебхука)». Тобто в тригері ви пишете, *що робити*, а вебхук приносить, *з чим*.\n\n**Два режими.** Задаються полем `sync` тригера:\n\n* `sync: false` (за замовчуванням) — `202 Accepted` одразу, хід іде у фоні. Правильний вибір майже завжди: чужі системи часто мають короткий таймаут, а хід агента може тривати хвилини.\n* `sync: true` — ми тримаємо з'єднання до кінця ходу й повертаємо `200` з відповіддю агента. Годиться, коли відповідь потрібна викликачу негайно; закладайте власний таймаут.\n\n**Обмеження.** Тіло — до 256 КіБ. Тригер має бути ввімкнений: вимкнений (зокрема автоматично — після 5 невдалих ходів поспіль) відповідає `409`.\n\n**Ідемпотентність.** Повторна доставка тієї самої події зробить **другий хід**: дедуплікації за тілом тут немає. Якщо ваше джерело може доставити подію двічі, тримайте ознаку обробленого у State агента.",
        "parameters": [
          {
            "name": "tid",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "examples": [
                "trg_01j9x2h7q0"
              ]
            },
            "description": "Ідентифікатор тригера з `hook_url`."
          },
          {
            "name": "X-Hook-Signature",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "examples": [
                "sha256=9b2f…"
              ]
            },
            "description": "`sha256=` + hex від `HMAC-SHA256(секрет тригера, сире тіло)`. Префікс `sha256=` можна не додавати — приймається і голий hex."
          }
        ],
        "requestBody": {
          "required": true,
          "description": "Довільне тіло вашої події, до 256 КіБ. Ми його не парсимо й не валідуємо — воно потрапляє в хід агента як текст.",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": true
              },
              "examples": {
                "default": {
                  "summary": "Подія вашої системи",
                  "value": {
                    "order_id": 4417,
                    "status": "delayed",
                    "customer": "olena@example.com"
                  }
                }
              }
            },
            "text/plain": {
              "schema": {
                "type": "string"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Синхронний режим (`sync: true`): хід завершився, відповідь агента — у `answer`.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "run_id",
                    "status"
                  ],
                  "properties": {
                    "run_id": {
                      "type": "string",
                      "description": "Ідентифікатор ходу. Той самий id видно в журналі ходів у кабінеті."
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "done"
                      ]
                    },
                    "answer": {
                      "type": "string",
                      "description": "Що агент відповів."
                    }
                  }
                },
                "examples": {
                  "default": {
                    "value": {
                      "run_id": "run_01j9x2m4p8",
                      "status": "done",
                      "answer": "Клієнта попереджено, доставку перенесено на завтра."
                    }
                  }
                }
              }
            }
          },
          "202": {
            "description": "Асинхронний режим (за замовчуванням): подію прийнято, хід іде у фоні. Результат дивіться в журналі ходів у кабінеті.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "enum": [
                        "accepted"
                      ]
                    }
                  }
                },
                "examples": {
                  "default": {
                    "value": {
                      "status": "accepted"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Не вдалося прочитати тіло запиту.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Підпис відсутній або не збігається. Найчастіша причина — HMAC порахували над переформатованим JSON, а не над сирим тілом.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "default": {
                    "value": {
                      "error": "невірний підпис",
                      "code": "unauthorized"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Такого вебхучного тригера немає (видалений або чужий id).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "default": {
                    "value": {
                      "error": "невідомий вебхук",
                      "code": "not_found"
                    }
                  }
                }
              }
            }
          },
          "409": {
            "description": "Тригер вимкнено. Або власник вимкнув його руками, або спрацювала автопауза після 5 невдалих ходів поспіль — увімкніть його в кабінеті.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "default": {
                    "value": {
                      "error": "тригер вимкнено",
                      "code": "conflict"
                    }
                  }
                }
              }
            }
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "502": {
            "description": "Синхронний режим: хід агента впав. `run_id` вкаже, який саме, — деталі в журналі ходів.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "run_id": {
                      "type": "string"
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "failed"
                      ]
                    }
                  }
                },
                "examples": {
                  "default": {
                    "value": {
                      "run_id": "run_01j9x2m4p8",
                      "status": "failed"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/openapi.json": {
      "get": {
        "tags": [
          "Службове"
        ],
        "summary": "Ця специфікація",
        "description": "Сам документ OpenAPI з підставленою адресою сервера. Віддається без автентифікації.",
        "security": [],
        "responses": {
          "200": {
            "description": "Специфікація."
          }
        }
      }
    },
    "/v1/telegram/webhook/{secret}": {
      "post": {
        "tags": [
          "Службове"
        ],
        "summary": "Вхідний вебхук Telegram",
        "description": "Кличе Telegram, не ви. Секрет у шляху — те, чим підтверджується походження виклику.",
        "security": [],
        "parameters": [
          {
            "name": "secret",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": true
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Прийнято."
          }
        }
      }
    },
    "/v1/widget/chat": {
      "post": {
        "tags": [
          "Віджет"
        ],
        "summary": "Репліка відвідувача",
        "description": "Те саме, що `POST /v1/api/chat`, але з публічним ключем і перевіркою домену. Відповідь так само має три режими: звичайна відповідь, розмову веде людина, вичерпано ліміт.",
        "security": [
          {
            "widgetKey": []
          }
        ],
        "parameters": [
          {
            "name": "X-Bot-Key",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "example": "pk_…"
            },
            "description": "Публічний ключ агента."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ChatRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Відповідь агента.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ChatResponse"
                }
              }
            }
          },
          "401": {
            "description": "Ключ невідомий.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Домен не дозволений.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Перевищено частоту.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/widget/chat/stream": {
      "post": {
        "tags": [
          "Віджет"
        ],
        "summary": "Репліка відвідувача (потоком)",
        "description": "`text/event-stream`: відповідь приходить частинами. Кадри описані схемою `StreamFrame`.",
        "security": [
          {
            "widgetKey": []
          }
        ],
        "parameters": [
          {
            "name": "X-Bot-Key",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "example": "pk_…"
            },
            "description": "Публічний ключ агента."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ChatRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Потік кадрів."
          },
          "401": {
            "description": "Ключ агента невідомий або відкликаний.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Ключ не того типу або домен не в переліку дозволених для цього агента.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/widget/config": {
      "get": {
        "tags": [
          "Віджет"
        ],
        "summary": "Налаштування віджета",
        "description": "Вигляд, вітання й доступність дій. Читається до першої репліки.",
        "security": [
          {
            "widgetKey": []
          }
        ],
        "parameters": [
          {
            "name": "X-Bot-Key",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "example": "pk_…"
            },
            "description": "Публічний ключ агента."
          }
        ],
        "responses": {
          "200": {
            "description": "Налаштування.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": true
                }
              }
            }
          },
          "401": {
            "description": "Ключ невідомий.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Домен не в переліку дозволених.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/widget/feedback": {
      "post": {
        "tags": [
          "Віджет"
        ],
        "summary": "Оцінити відповідь",
        "description": "Великий/малий палець під реплікою агента.",
        "security": [
          {
            "widgetKey": []
          }
        ],
        "parameters": [
          {
            "name": "X-Bot-Key",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "example": "pk_…"
            },
            "description": "Публічний ключ агента."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "session_id": {
                    "type": "string"
                  },
                  "message_id": {
                    "type": "string"
                  },
                  "up": {
                    "type": "boolean"
                  }
                },
                "required": [
                  "session_id",
                  "up"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Зараховано."
          },
          "401": {
            "description": "Ключ агента невідомий або відкликаний.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Ключ не того типу або домен не в переліку дозволених для цього агента.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/widget/handoff": {
      "post": {
        "tags": [
          "Віджет"
        ],
        "summary": "Покликати людину",
        "description": "Ставить розмову в чергу операторів.",
        "security": [
          {
            "widgetKey": []
          }
        ],
        "parameters": [
          {
            "name": "X-Bot-Key",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "example": "pk_…"
            },
            "description": "Публічний ключ агента."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "session_id": {
                    "type": "string"
                  }
                },
                "required": [
                  "session_id"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Передано.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HandoffStatus"
                }
              }
            }
          },
          "401": {
            "description": "Ключ агента невідомий або відкликаний.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Ключ не того типу або домен не в переліку дозволених для цього агента.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/widget/lead": {
      "post": {
        "tags": [
          "Віджет"
        ],
        "summary": "Лишити контакт",
        "description": "Імʼя й спосіб звʼязку, які потім видно в кабінеті.",
        "security": [
          {
            "widgetKey": []
          }
        ],
        "parameters": [
          {
            "name": "X-Bot-Key",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "example": "pk_…"
            },
            "description": "Публічний ключ агента."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "session_id": {
                    "type": "string"
                  },
                  "name": {
                    "type": "string"
                  },
                  "contact": {
                    "type": "string"
                  },
                  "message": {
                    "type": "string"
                  }
                },
                "required": [
                  "contact"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Збережено."
          },
          "401": {
            "description": "Ключ агента невідомий або відкликаний.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Ключ не того типу або домен не в переліку дозволених для цього агента.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/widget/messages": {
      "get": {
        "tags": [
          "Віджет"
        ],
        "summary": "Нові повідомлення",
        "description": "Опитування, доки розмову веде оператор.",
        "security": [
          {
            "widgetKey": []
          }
        ],
        "parameters": [
          {
            "name": "X-Bot-Key",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "example": "pk_…"
            },
            "description": "Публічний ключ агента."
          },
          {
            "name": "session_id",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Повідомлення.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "messages": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Message"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Ключ агента невідомий або відкликаний.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Ключ не того типу або домен не в переліку дозволених для цього агента.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/widget/messages/stream": {
      "get": {
        "tags": [
          "Віджет"
        ],
        "summary": "Нові повідомлення (потоком)",
        "description": "SSE замість опитування: те саме, але без затримки й зайвих запитів.",
        "security": [
          {
            "widgetKey": []
          }
        ],
        "parameters": [
          {
            "name": "X-Bot-Key",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "example": "pk_…"
            },
            "description": "Публічний ключ агента."
          },
          {
            "name": "session_id",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Потік."
          },
          "401": {
            "description": "Ключ агента невідомий або відкликаний.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Ключ не того типу або домен не в переліку дозволених для цього агента.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/widget/typing": {
      "post": {
        "tags": [
          "Віджет"
        ],
        "summary": "Відвідувач друкує",
        "description": "Показує оператору індикатор набору.",
        "security": [
          {
            "widgetKey": []
          }
        ],
        "parameters": [
          {
            "name": "X-Bot-Key",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "example": "pk_…"
            },
            "description": "Публічний ключ агента."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "session_id": {
                    "type": "string"
                  }
                },
                "required": [
                  "session_id"
                ]
              }
            }
          }
        },
        "responses": {
          "204": {
            "description": "Прийнято."
          },
          "401": {
            "description": "Ключ агента невідомий або відкликаний.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Ключ не того типу або домен не в переліку дозволених для цього агента.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/widget/upload": {
      "post": {
        "tags": [
          "Віджет"
        ],
        "summary": "Вкладення від відвідувача",
        "description": "Файл, який відвідувач додає до розмови.",
        "security": [
          {
            "widgetKey": []
          }
        ],
        "parameters": [
          {
            "name": "X-Bot-Key",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "example": "pk_…"
            },
            "description": "Публічний ключ агента."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary"
                  },
                  "session_id": {
                    "type": "string"
                  }
                },
                "required": [
                  "file"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Завантажено.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "url": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Ключ агента невідомий або відкликаний.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Ключ не того типу або домен не в переліку дозволених для цього агента.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "Файл завеликий.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/version": {
      "get": {
        "tags": [
          "Службове"
        ],
        "summary": "Версія збірки",
        "description": "Коміт, з якого зібрано процес.",
        "security": [],
        "responses": {
          "200": {
            "description": "Версія.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "version": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "apiKeyAuth": {
        "type": "apiKey",
        "in": "header",
        "name": "X-Bot-Key",
        "description": "Секретний ключ бота (`sk_…`) з кабінету YouSelfBot. Тримайте його на бекенді — ніколи не показуйте у браузері. Публічний ключ `pk_…` зі сніпета віджета тут не працює: буде `403` `wrong_key_type`."
      },
      "sessionCookie": {
        "type": "apiKey",
        "in": "cookie",
        "name": "session",
        "description": "Сесійна кука кабінету. Ставиться на `POST /v1/auth/login` і живе на всіх піддоменах продукту. Небезпечні методи додатково вимагають CSRF-заголовка."
      },
      "widgetKey": {
        "type": "apiKey",
        "in": "header",
        "name": "X-Bot-Key",
        "description": "Публічний ключ агента (`pk_…`). Призначений для браузера: сам по собі дозволяє лише розмову й читання налаштувань віджета."
      }
    },
    "schemas": {
      "SessionID": {
        "type": "string",
        "description": "Ідентифікатор розмови. Має бути **непередбачуваним** і унікальним у межах бота — рекомендовано UUIDv4. Не використовуйте id користувача, email, номер замовлення чи лічильники: передбачувані id стикаються між вашими власними користувачами. Максимум 128 символів.",
        "examples": [
          "3f6c1b9e-7f42-4d2a-9a6f-0d5b1f0b21c7"
        ],
        "maxLength": 128
      },
      "ChatRequest": {
        "type": "object",
        "required": [
          "message"
        ],
        "properties": {
          "session_id": {
            "allOf": [
              {
                "$ref": "#/components/schemas/SessionID"
              }
            ],
            "description": "Ідентифікатор розмови: той самий у межах одного діалогу — так бот памʼятає контекст. Якщо не передати, сервер згенерує випадковий і поверне у відповіді. Має бути непередбачуваним (UUIDv4), не id користувача."
          },
          "message": {
            "type": "string",
            "minLength": 1,
            "description": "Повідомлення користувача. Порожнє → 400. Максимум 8000 символів (рахуються символи, не байти) → 400.",
            "maxLength": 8000
          },
          "user_token": {
            "type": "string",
            "description": "Ідентифікатор кінцевого користувача на вашому боці — саме тут місце для вашого внутрішнього id (на відміну від `session_id`). Використовується для памʼяті про користувача та підстановки у виклики скілів; ніколи не надсилається в LLM.\n\nЦе поле НЕ перевіряється: воно просто передається у ваш власний API, який його й автентифікує. Тому воно не дає жодних додаткових прав. Якщо потрібно надати комусь дії, недоступні звичайним відвідувачам, використовуйте `user_auth` нижче."
          },
          "user_auth": {
            "type": "string",
            "description": "Підтвердження особи відвідувача, сформоване вашим сервером (див. розділ «Підтвердження особи відвідувача» в описі API). Єдиний спосіб надати відвідувачу дії з позначкою «лише для адміністратора».\n\nПорожнє, прострочене, видане для іншого бота або непідтверджене значення не є помилкою: відвідувач лишається звичайним, а розмова відповідає як завжди."
          },
          "page_context": {
            "type": "string",
            "description": "Короткий опис сторінки/контексту, де користувач зараз (URL, товар, ціна). Дає боту конкретні, контекстні відповіді."
          },
          "escalate": {
            "type": "boolean",
            "description": "Явний запит на живого оператора (кнопка «оператор»). `true` → одразу ставить у чергу handoff; відповідь матиме `handoff: true`."
          }
        }
      },
      "ChatResponse": {
        "type": "object",
        "required": [
          "answer",
          "session_id"
        ],
        "properties": {
          "answer": {
            "type": "string",
            "description": "Відповідь бота. При `handoff: true` це підтвердження ескалації або **порожній рядок** (оператор уже на звʼязку — показувати нічого). При `quota_exceeded: true` — текст-заглушка."
          },
          "session_id": {
            "allOf": [
              {
                "$ref": "#/components/schemas/SessionID"
              }
            ],
            "description": "Ідентифікатор розмови (згенерований, якщо не передавали). Використовуйте його в наступних запитах."
          },
          "handoff": {
            "type": "boolean",
            "description": "Присутнє і `true` → розмову веде або чекає живий оператор, бот навмисно мовчить. Опитуйте `GET /v1/api/messages`. У звичайній відповіді поле відсутнє."
          },
          "quota_exceeded": {
            "type": "boolean",
            "description": "Присутнє і `true` → вичерпано місячний ліміт розмов, нову розмову не розпочато. Це `200`, а не помилка. Уже розпочаті розмови продовжують працювати. У звичайній відповіді поле відсутнє."
          },
          "notice": {
            "type": "string",
            "enum": [
              "outage"
            ],
            "description": "Присутнє і `\"outage\"` → це НЕ репліка бота, а службове повідомлення про збій або вичерпаний ліміт. Показуйте його окремо від відповідей (без оцінок 👍/👎) — інакше клієнт оцінює несправність. У звичайній відповіді поле відсутнє. Значення можуть додаватися: невідоме трактуйте як службове повідомлення."
          }
        }
      },
      "StreamFrame": {
        "description": "Один кадр SSE — JSON у полі `data:`. Розрізняйте за наявним полем (`delta` / `action` / `error` / `done`). Перелік видів кадру може поповнюватися: кадр без жодного зі знайомих полів **пропускайте**, а не падайте.",
        "oneOf": [
          {
            "$ref": "#/components/schemas/StreamDelta"
          },
          {
            "$ref": "#/components/schemas/StreamAction"
          },
          {
            "$ref": "#/components/schemas/StreamError"
          },
          {
            "$ref": "#/components/schemas/StreamDone"
          }
        ]
      },
      "StreamDelta": {
        "type": "object",
        "title": "Фрагмент відповіді",
        "required": [
          "delta"
        ],
        "properties": {
          "delta": {
            "type": "string",
            "description": "Черговий шматок тексту — дописуйте до вже отриманого."
          },
          "notice": {
            "type": "string",
            "enum": [
              "outage"
            ],
            "description": "Присутнє і `\"outage\"` → текст у `delta` є службовим повідомленням про збій чи ліміт, а не реплікою бота. Показуйте його окремо, без оцінок."
          }
        },
        "examples": [
          {
            "delta": "Ми працюємо "
          },
          {
            "delta": "Вибачте, бот тимчасово недоступний. Спробуйте, будь ласка, пізніше.",
            "notice": "outage"
          }
        ]
      },
      "StreamError": {
        "type": "object",
        "title": "Помилка у стрімі",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "string",
            "description": "Текст помилки для людини."
          },
          "code": {
            "type": "string",
            "enum": [
              "llm_unavailable",
              "llm_overloaded",
              "internal_error"
            ],
            "description": "Стабільний код помилки. `llm_unavailable` — ключ або кошти, потрібні дії власника; `llm_overloaded` — перевантаження або вичерпаний час ходу, має сенс повторити; `internal_error` — збій на нашому боці. Перелік може поповнюватися — незнайомий код обробляйте як загальну помилку."
          },
          "retry_after": {
            "type": "integer",
            "description": "Скільки секунд зачекати перед повтором. Приходить лише з `llm_overloaded` (у стрімі заголовків уже немає, тож `Retry-After` їде в кадрі)."
          }
        },
        "examples": [
          {
            "error": "Вибачте, асистент тимчасово недоступний. Ми вже знаємо про це — спробуйте, будь ласка, трохи згодом.",
            "code": "llm_unavailable"
          },
          {
            "error": "Вибачте, зараз надто багато запитів і відповідь не встигла надійти. Надішліть, будь ласка, повідомлення ще раз.",
            "code": "llm_overloaded",
            "retry_after": 20
          },
          {
            "error": "internal error"
          }
        ]
      },
      "StreamDone": {
        "type": "object",
        "title": "Кінець потоку",
        "required": [
          "done",
          "session_id"
        ],
        "properties": {
          "done": {
            "type": "boolean",
            "const": true
          },
          "session_id": {
            "allOf": [
              {
                "$ref": "#/components/schemas/SessionID"
              }
            ],
            "description": "Ідентифікатор розмови — згенерований, якщо ви його не передавали."
          },
          "handoff": {
            "type": "boolean",
            "description": "`true` → розмову веде або чекає живий оператор (те саме, що `handoff` у `POST /v1/api/chat`)."
          },
          "quota_exceeded": {
            "type": "boolean",
            "description": "`true` → вичерпано місячний ліміт розмов; текст-заглушка вже прийшов кадром `delta`."
          },
          "notice": {
            "type": "string",
            "enum": [
              "outage"
            ],
            "description": "Присутнє і `\"outage\"` → текст цього ходу був службовим повідомленням про збій, а не реплікою бота. Приходить і в кінцевому кадрі, бо частину таких повідомлень видно лише після завершення ходу — перекласифікуйте вже показане."
          }
        },
        "examples": [
          {
            "done": true,
            "session_id": "3f6c1b9e-7f42-4d2a-9a6f-0d5b1f0b21c7"
          }
        ]
      },
      "Message": {
        "type": "object",
        "required": [
          "id",
          "author",
          "text",
          "at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Ідентифікатор повідомлення."
          },
          "author": {
            "type": "string",
            "enum": [
              "visitor",
              "bot",
              "operator"
            ],
            "description": "Хто автор повідомлення."
          },
          "text": {
            "type": "string"
          },
          "at": {
            "type": "integer",
            "format": "int64",
            "description": "Час (Unix ms). Передавайте це значення (з останнього повідомлення) як `after` у наступному запиті `GET /v1/api/messages`."
          },
          "operator_id": {
            "type": "string",
            "description": "Хто з операторів написав. Лише для `author: \"operator\"`."
          },
          "operator_name": {
            "type": "string",
            "description": "Імʼя оператора для показу відвідувачу («Олена з підтримки»). Лише для `author: \"operator\"`, і лише якщо його заповнено."
          }
        }
      },
      "HandoffStatus": {
        "type": "string",
        "enum": [
          "bot",
          "waiting",
          "operator",
          "resolved"
        ],
        "description": "bot — відповідає бот; waiting — у черзі до оператора; operator — оператор на звʼязку; resolved — завершено. Список може поповнюватися — незнайоме значення трактуйте як `bot`."
      },
      "Thread": {
        "type": "object",
        "required": [
          "session_id",
          "bot_id",
          "status",
          "last_text",
          "updated_at",
          "activity_at"
        ],
        "properties": {
          "session_id": {
            "allOf": [
              {
                "$ref": "#/components/schemas/SessionID"
              }
            ],
            "description": "Ідентифікатор розмови."
          },
          "bot_id": {
            "type": "string",
            "description": "Бот, якому належить розмова (завжди бот вашого ключа)."
          },
          "status": {
            "$ref": "#/components/schemas/HandoffStatus"
          },
          "last_text": {
            "type": "string",
            "description": "Останнє повідомлення у розмові."
          },
          "updated_at": {
            "type": "integer",
            "format": "int64",
            "description": "Час останньої активності (Unix ms)."
          },
          "assignee_id": {
            "type": "string",
            "description": "Оператор, який узяв звернення. Відсутнє, поки нікому не призначено."
          },
          "assignee_name": {
            "type": "string",
            "description": "Імʼя цього оператора для показу."
          },
          "assigned_at": {
            "type": "integer",
            "format": "int64",
            "description": "Коли звернення призначили (Unix ms)."
          },
          "activity_at": {
            "type": "integer",
            "format": "int64",
            "description": "Час останньої активності будь-якої сторони (Unix ms) — присутній завжди. На відміну від `updated_at`, зсувається і від сигналів «друкує»."
          }
        }
      },
      "Source": {
        "type": "object",
        "required": [
          "id",
          "title",
          "chunks"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Ідентифікатор джерела — той, що передали як `source_id`, або згенерований."
          },
          "title": {
            "type": "string"
          },
          "chunks": {
            "type": "integer",
            "description": "На скільки фрагментів розбито джерело."
          }
        }
      },
      "Error": {
        "type": "object",
        "required": [
          "error",
          "code"
        ],
        "properties": {
          "error": {
            "type": "string",
            "description": "Текст помилки для людини. Може змінюватися будь-коли — не парсьте його і не будуйте на ньому логіку."
          },
          "code": {
            "type": "string",
            "description": "Стабільний машинний код помилки — на нього і розгалужуйтесь. Присутній у КОЖНІЙ JSON-помилці цього API, включно з кадрами помилок у стрімі. Перелік може поповнюватися: незнайомий код обробляйте як загальну помилку відповідного HTTP-статусу, а не падайте.",
            "enum": [
              "bad_request",
              "missing_key",
              "invalid_key",
              "unauthorized",
              "forbidden",
              "wrong_key_type",
              "not_found",
              "conflict",
              "rate_limited",
              "internal_error",
              "llm_unavailable",
              "llm_overloaded",
              "service_unavailable",
              "payload_too_large",
              "idempotency_in_progress",
              "idempotency_key_reused"
            ]
          },
          "session_id": {
            "type": "string",
            "description": "Ідентифікатор розмови, у якій сталася помилка. Присутній у помилках `/v1/api/chat` (503), щоб ви могли звʼязати збій із конкретним діалогом."
          },
          "retry_after": {
            "type": "integer",
            "description": "Скільки секунд зачекати перед повтором. Приходить із `llm_overloaded`; дублює заголовок `Retry-After`."
          }
        },
        "examples": [
          {
            "error": "wrong key type: this endpoint needs a secret key (sk_)",
            "code": "wrong_key_type"
          }
        ]
      },
      "HandoffEvent": {
        "type": "object",
        "description": "Тіло вихідного вебхука. Одна форма на всі три події — набір заповнених полів залежить від `event`.",
        "required": [
          "event",
          "bot_id",
          "session_id",
          "at"
        ],
        "properties": {
          "event": {
            "type": "string",
            "enum": [
              "escalated",
              "visitor_message",
              "resolved"
            ],
            "description": "Що сталося:\n\n* `escalated` — розмову передано людині; агент замовк і чекає на оператора.\n* `visitor_message` — відвідувач написав у вже переданій розмові.\n* `resolved` — звернення закрито; далі розмову знову веде агент.\n\nПерелік може поповнюватися: незнайому подію ігноруйте, а не падайте на ній."
          },
          "bot_id": {
            "type": "string",
            "description": "Ідентифікатор агента, чию розмову передано."
          },
          "session_id": {
            "type": "string",
            "description": "Ідентифікатор розмови. Це той самий `session_id`, з яким далі працює операторське API: `GET /v1/api/operator/threads/{sid}`, `POST /v1/api/operator/threads/{sid}/reply`."
          },
          "text": {
            "type": "string",
            "description": "Репліка відвідувача. Заповнене лише для `visitor_message`; в інших подіях відсутнє."
          },
          "transcript": {
            "type": "string",
            "description": "Недавній контекст розмови одним текстом — щоб оператор одразу бачив, з чого все почалось. Заповнене лише для `escalated`."
          },
          "at": {
            "type": "integer",
            "format": "int64",
            "description": "Час події, Unix-мілісекунди."
          }
        }
      },
      "StreamAction": {
        "type": "object",
        "title": "Прогрес дії у стрімі",
        "description": "Агент виконує дію (звернення до вашого API, довга робота) і ще не почав відповідати. Кадр існує, щоб у чаті не було мовчазної паузи: показуйте його як статус-рядок і замінюйте, коли підуть `delta`.\n\nКадр **необовʼязковий** — приходить лише коли хід справді викликає дії. Клієнт, який його не розуміє, має просто пропустити кадр, а не впасти.",
        "required": [
          "action"
        ],
        "properties": {
          "action": {
            "type": "string",
            "description": "Машинний стан дії — придатний для показу як «виконую…»."
          },
          "action_count": {
            "type": "integer",
            "description": "Скільки дій виконано в цьому ході. Присутнє не завжди."
          }
        }
      },
      "Task": {
        "type": "object",
        "description": "Картка довгої дії агента.",
        "required": [
          "id",
          "title",
          "status"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Ідентифікатор роботи."
          },
          "title": {
            "type": "string",
            "description": "Що робиться — текст для показу."
          },
          "status": {
            "type": "string",
            "description": "Стан роботи."
          },
          "detail": {
            "type": "string",
            "description": "Уточнення до стану, якщо є."
          },
          "note": {
            "type": "string",
            "description": "Примітка для відвідувача, якщо є."
          },
          "started_at": {
            "type": "integer",
            "format": "int64",
            "description": "Початок (Unix ms)."
          },
          "updated_at": {
            "type": "integer",
            "format": "int64",
            "description": "Останнє оновлення (Unix ms)."
          }
        }
      },
      "StepRef": {
        "type": "object",
        "properties": {
          "type": {
            "type": "string",
            "description": "Тип кроку з реєстру, напр. `knowledge_retrieve`.",
            "example": "knowledge_retrieve"
          },
          "id": {
            "type": "string",
            "description": "Власне імʼя кроку в пайплайні. Пресети чіпляються якорями саме до нього, тож перейменування кроку розриває якір."
          },
          "origin": {
            "type": "string",
            "description": "Звідки крок узявся: `user` або `preset:<id>@<версія>`. Живе в самому кроці, тому переживає збереження версії, і зняття пресета не чіпає ручних кроків."
          },
          "config": {
            "type": "object",
            "additionalProperties": true,
            "description": "Конфіг кроку. Форму описує `ConfigSchema` з каталогу `/v1/dashboard/pipeline/steps`."
          }
        },
        "required": [
          "type"
        ]
      },
      "Pipeline": {
        "type": "object",
        "properties": {
          "pre": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/StepRef"
            },
            "description": "До першого звернення до моделі."
          },
          "model": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/StepRef"
            },
            "description": "Навколо циклу «модель ↔ інструменти»."
          },
          "post": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/StepRef"
            },
            "description": "Після того, як відповідь готова."
          }
        }
      },
      "StepSpec": {
        "type": "object",
        "properties": {
          "Type": {
            "type": "string"
          },
          "Phases": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Фази, у яких крок дозволений."
          },
          "Reads": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Що крок читає з контексту ходу."
          },
          "Writes": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Що крок у контекст кладе."
          },
          "Requires": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Що має бути підключено, щоб крок працював."
          },
          "CanHalt": {
            "type": "boolean",
            "description": "Чи може крок обірвати хід, не будячи модель."
          },
          "Cost": {
            "type": "string",
            "enum": [
              "free",
              "llm",
              "http"
            ],
            "description": "Чого коштує крок."
          }
        }
      },
      "AgentVersion": {
        "type": "object",
        "properties": {
          "number": {
            "type": "integer"
          },
          "state": {
            "type": "string",
            "enum": [
              "draft",
              "live",
              "archived"
            ]
          },
          "pipeline": {
            "$ref": "#/components/schemas/Pipeline"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "published_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "PipelineDiff": {
        "type": "object",
        "properties": {
          "added": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "removed": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "moved": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "Trigger": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "enum": [
              "schedule",
              "webhook",
              "poll",
              "manual"
            ]
          },
          "name": {
            "type": "string"
          },
          "enabled": {
            "type": "boolean"
          },
          "input": {
            "type": "string",
            "description": "Задача, з якою будиться агент."
          },
          "cron": {
            "type": "string",
            "description": "Лише для `schedule`. Виконується в таймзоні агента, не в UTC."
          },
          "timezone": {
            "type": "string",
            "example": "Europe/Kyiv"
          },
          "sync": {
            "type": "boolean",
            "description": "Лише для `webhook`: чекати на відповідь агента замість 202."
          },
          "interval_sec": {
            "type": "integer",
            "description": "Лише для `poll`, від 30."
          },
          "fail_streak": {
            "type": "integer",
            "description": "Скільки запусків поспіль впало. Пʼять — тригер стає на паузу, власнику йде сповіщення."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "hook_url": {
            "type": "string",
            "description": "Лише для `webhook`: адреса, на яку слати подію."
          }
        }
      },
      "Run": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "example": "run_01927d3f"
          },
          "trigger": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "running",
              "done",
              "failed"
            ]
          },
          "error": {
            "type": "string"
          },
          "credits": {
            "type": "number"
          },
          "input_tokens": {
            "type": "integer"
          },
          "output_tokens": {
            "type": "integer"
          },
          "started_at": {
            "type": "string",
            "format": "date-time"
          },
          "finished_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "AgentState": {
        "type": "object",
        "properties": {
          "values": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            }
          },
          "version": {
            "type": "integer",
            "description": "Версія для CAS. Передайте її назад у `PUT`; розбіжність = 409."
          }
        }
      },
      "AgentPreset": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "version": {
            "type": "integer"
          },
          "title": {
            "type": "string"
          },
          "summary": {
            "type": "string"
          },
          "conflicts_with": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "Member": {
        "type": "object",
        "properties": {
          "user_id": {
            "type": "string"
          },
          "email": {
            "type": "string",
            "format": "email"
          },
          "role": {
            "type": "string",
            "enum": [
              "owner",
              "admin",
              "operator"
            ]
          },
          "you": {
            "type": "boolean"
          }
        }
      },
      "Invite": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "email": {
            "type": "string",
            "format": "email"
          },
          "role": {
            "type": "string"
          },
          "expires_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Skill": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "method": {
            "type": "string"
          },
          "url": {
            "type": "string"
          },
          "admin_only": {
            "type": "boolean",
            "description": "Дія доступна лише авторизованому власнику сайту, не анонімному відвідувачу."
          }
        }
      },
      "Lead": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string"
          },
          "contact": {
            "type": "string"
          },
          "message": {
            "type": "string"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      }
    },
    "responses": {
      "BadRequest": {
        "description": "Некоректний запит: відсутнє або порожнє обовʼязкове поле, або тіло не є валідним JSON.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "default": {
                "value": {
                  "error": "message required",
                  "code": "bad_request"
                }
              }
            }
          }
        }
      },
      "Unauthorized": {
        "description": "Відсутній або недійсний `X-Bot-Key`. Потрібен секретний ключ (`sk_…`) з кабінету YouSelfBot.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "default": {
                "value": {
                  "error": "missing X-Bot-Key",
                  "code": "missing_key"
                }
              },
              "invalidKey": {
                "summary": "Ключ невідомий або відкликаний",
                "value": {
                  "error": "invalid key",
                  "code": "invalid_key"
                }
              }
            }
          }
        }
      },
      "Forbidden": {
        "description": "Ключ дійсний, але не того типу: ви передали публічний ключ (`pk_…`) зі сніпета віджета там, де потрібен секретний (`sk_…`). Візьміть секретний ключ у кабінеті YouSelfBot і викликайте API зі свого бекенду. Це найчастіша помилка на старті — і це `403`, а не `401`.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "wrongKeyType": {
                "value": {
                  "error": "wrong key type: this endpoint needs a secret key (sk_)",
                  "code": "wrong_key_type"
                }
              }
            }
          }
        }
      },
      "RateLimited": {
        "description": "Перевищено ліміт запитів: 60/хв на діалог і записи, 240/хв на читання та сигнали (вікно 1 хвилина, окремо на пару ключ+IP). Ті самі заголовки `RateLimit-Limit` / `RateLimit-Remaining` / `RateLimit-Reset` ставляться і на **успішні** відповіді — стежте за ними, не чекаючи на 429. Дублікати з префіксом `X-` присутні для сумісності зі старими клієнтами. Повторіть за кілька секунд з експоненційною витримкою.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "limited": {
                "value": {
                  "error": "rate limit exceeded",
                  "code": "rate_limited"
                }
              }
            }
          }
        },
        "headers": {
          "Retry-After": {
            "schema": {
              "type": "integer"
            },
            "description": "Скільки секунд зачекати перед повтором."
          },
          "RateLimit-Limit": {
            "schema": {
              "type": "integer"
            },
            "description": "Скільки запитів дозволено у вікні."
          },
          "RateLimit-Remaining": {
            "schema": {
              "type": "integer"
            },
            "description": "Скільки лишилось у поточному вікні."
          },
          "RateLimit-Reset": {
            "schema": {
              "type": "integer"
            },
            "description": "Через скільки секунд вікно оновиться."
          }
        }
      },
      "InternalError": {
        "description": "Внутрішня помилка сервера. Запит можна безпечно повторити.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "default": {
                "value": {
                  "error": "internal error",
                  "code": "internal_error"
                }
              }
            }
          }
        }
      },
      "LLMUnavailable": {
        "description": "ШІ-бекенд тимчасово недоступний — бот не може згенерувати відповідь. Ми вже сповіщені. Покажіть клієнту дружнє повідомлення й повторіть пізніше. У тілі додатково приходить `session_id`, тож розмову можна продовжити тим самим ідентифікатором.",
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "required": [
                "error",
                "code",
                "session_id"
              ],
              "properties": {
                "error": {
                  "type": "string"
                },
                "code": {
                  "type": "string",
                  "const": "llm_unavailable"
                },
                "session_id": {
                  "$ref": "#/components/schemas/SessionID"
                }
              }
            },
            "examples": {
              "unavailable": {
                "value": {
                  "error": "Вибачте, асистент тимчасово недоступний. Ми вже знаємо про це — спробуйте, будь ласка, трохи згодом.",
                  "code": "llm_unavailable",
                  "session_id": "3f6c1b9e-7f42-4d2a-9a6f-0d5b1f0b21c7"
                }
              }
            }
          }
        }
      },
      "PayloadTooLarge": {
        "description": "Тіло запиту завелике (1 МіБ для більшості ендпоїнтів, 5 МіБ для `POST /v1/api/knowledge`).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "default": {
                "value": {
                  "error": "payload too large (max 1024 KiB)",
                  "code": "payload_too_large"
                }
              }
            }
          }
        }
      },
      "IdempotencyConflict": {
        "description": "Конфлікт `Idempotency-Key`: або запит із цим ключем ще виконується (`idempotency_in_progress` — повторіть за секунду), або цей ключ уже використано для ІНШОГО запиту (`idempotency_key_reused` — візьміть новий ключ).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "inProgress": {
                "value": {
                  "error": "a request with this Idempotency-Key is still in progress",
                  "code": "idempotency_in_progress"
                }
              },
              "reused": {
                "value": {
                  "error": "Idempotency-Key was already used for a different request",
                  "code": "idempotency_key_reused"
                }
              }
            }
          }
        }
      }
    },
    "parameters": {
      "PageLimit": {
        "name": "limit",
        "in": "query",
        "required": false,
        "schema": {
          "type": "integer",
          "minimum": 1,
          "maximum": 200,
          "default": 50
        },
        "description": "Скільки елементів повернути (1–200, типово 50). Більше за 200 — обрізається до 200, помилки не буде.",
        "example": 50
      },
      "PageCursor": {
        "name": "cursor",
        "in": "query",
        "required": false,
        "schema": {
          "type": "string"
        },
        "description": "Непрозорий курсор наступної сторінки: передайте `next_cursor` з попередньої відповіді. Порожньо/відсутній — перша сторінка. Не розбирайте його вміст: формат може змінитися."
      },
      "IdempotencyKey": {
        "name": "Idempotency-Key",
        "in": "header",
        "required": false,
        "schema": {
          "type": "string",
          "maxLength": 255
        },
        "description": "Унікальний ключ цього запиту (наприклад UUIDv4). Повтор із тим самим ключем НЕ виконує дію вдруге — повертається збережена відповідь першої спроби із заголовком `Idempotency-Replayed: true`. Зберігається 24 години.",
        "example": "3f6c1b9e-7f42-4d2a-9a6f-0d5b1f0b21c7"
      }
    }
  },
  "webhooks": {
    "escalated": {
      "post": {
        "summary": "Розмову передано живому оператору",
        "description": "Агент віддав розмову людині: відвідувач натиснув «покликати оператора» або агент сам вирішив, що далі має говорити людина.\n\nУ `transcript` — недавній контекст розмови, щоб оператор не починав з нуля. Далі ваш бекенд відповідає через `POST /v1/api/operator/threads/{sid}/reply` і закриває звернення через `.../resolve`.\n\nРазом із подіями `visitor_message` і `resolved` це дає повний цикл **без полінгу** `GET /v1/api/operator/threads`.\n\nПідпис перевіряйте до розбору тіла (див. розділ «Вихідні вебхуки: перевірка підпису» у вступі). Доставка повторюється при `5xx` і мережевих помилках — обробник мусить бути ідемпотентним.",
        "operationId": "webhookEscalated",
        "security": [],
        "parameters": [
          {
            "name": "X-YouSelfBot-Timestamp",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Час доставки, Unix-секунди. Входить у підписаний рядок."
          },
          {
            "name": "X-YouSelfBot-Signature",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "examples": [
                "sha256=6f1c…"
              ]
            },
            "description": "`sha256=` + hex від `HMAC-SHA256(секрет, \"<timestamp>.<сире тіло>\")`."
          },
          {
            "name": "User-Agent",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string",
              "examples": [
                "YouSelfBot-Handoff/1.0"
              ]
            },
            "description": "Завжди `YouSelfBot-Handoff/1.0`."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/HandoffEvent"
              },
              "examples": {
                "default": {
                  "value": {
                    "event": "escalated",
                    "bot_id": "bot_7f3a1c",
                    "session_id": "3f6c1b9e-7f42-4d2a-9a6f-0d5b1f0b21c7",
                    "transcript": "Клієнт: Замовлення №4417 досі не приїхало\nБот: За даними перевізника посилка в дорозі, орієнтовно завтра.\nКлієнт: Мені це вже казали тиждень тому. Покличте людину.",
                    "at": 1753600000123
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Прийнято. Будь-який `2xx`/`3xx` вважається успіхом і не повторюється."
          },
          "400": {
            "description": "Будь-який `4xx` — остаточна відмова: ми НЕ повторюємо доставку. Якщо хочете ретрай, відповідайте `5xx`."
          },
          "500": {
            "description": "Тимчасовий збій. Ми повторимо доставку тричі — через 1 с, 5 с і 25 с."
          }
        }
      }
    },
    "visitor_message": {
      "post": {
        "summary": "Відвідувач написав у переданій розмові",
        "description": "Нова репліка відвідувача в розмові, яку вже веде людина. Текст — у `text`.\n\nЦя подія приходить лише поки триває передача: коли звернення закрито (`resolved`), відповідає знову агент, і подій більше немає.\n\nПідпис перевіряйте до розбору тіла (див. розділ «Вихідні вебхуки: перевірка підпису» у вступі). Доставка повторюється при `5xx` і мережевих помилках — обробник мусить бути ідемпотентним.",
        "operationId": "webhookVisitorMessage",
        "security": [],
        "parameters": [
          {
            "name": "X-YouSelfBot-Timestamp",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Час доставки, Unix-секунди. Входить у підписаний рядок."
          },
          {
            "name": "X-YouSelfBot-Signature",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "examples": [
                "sha256=6f1c…"
              ]
            },
            "description": "`sha256=` + hex від `HMAC-SHA256(секрет, \"<timestamp>.<сире тіло>\")`."
          },
          {
            "name": "User-Agent",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string",
              "examples": [
                "YouSelfBot-Handoff/1.0"
              ]
            },
            "description": "Завжди `YouSelfBot-Handoff/1.0`."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/HandoffEvent"
              },
              "examples": {
                "default": {
                  "value": {
                    "event": "visitor_message",
                    "bot_id": "bot_7f3a1c",
                    "session_id": "3f6c1b9e-7f42-4d2a-9a6f-0d5b1f0b21c7",
                    "text": "Номер накладної 59000123456789, перевірте, будь ласка",
                    "at": 1753600041880
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Прийнято. Будь-який `2xx`/`3xx` вважається успіхом і не повторюється."
          },
          "400": {
            "description": "Будь-який `4xx` — остаточна відмова: ми НЕ повторюємо доставку. Якщо хочете ретрай, відповідайте `5xx`."
          },
          "500": {
            "description": "Тимчасовий збій. Ми повторимо доставку тричі — через 1 с, 5 с і 25 с."
          }
        }
      }
    },
    "resolved": {
      "post": {
        "summary": "Звернення закрито",
        "description": "Розмову повернуто агенту — оператор натиснув «завершити» в кабінеті або ваш бекенд викликав `POST /v1/api/operator/threads/{sid}/resolve`.\n\nПодія приходить і в тому разі, коли закриття ініціювали ви самі: так дві операторські системи (наш кабінет і ваша) лишаються синхронними.\n\nПідпис перевіряйте до розбору тіла (див. розділ «Вихідні вебхуки: перевірка підпису» у вступі). Доставка повторюється при `5xx` і мережевих помилках — обробник мусить бути ідемпотентним.",
        "operationId": "webhookResolved",
        "security": [],
        "parameters": [
          {
            "name": "X-YouSelfBot-Timestamp",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Час доставки, Unix-секунди. Входить у підписаний рядок."
          },
          {
            "name": "X-YouSelfBot-Signature",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "examples": [
                "sha256=6f1c…"
              ]
            },
            "description": "`sha256=` + hex від `HMAC-SHA256(секрет, \"<timestamp>.<сире тіло>\")`."
          },
          {
            "name": "User-Agent",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string",
              "examples": [
                "YouSelfBot-Handoff/1.0"
              ]
            },
            "description": "Завжди `YouSelfBot-Handoff/1.0`."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/HandoffEvent"
              },
              "examples": {
                "default": {
                  "value": {
                    "event": "resolved",
                    "bot_id": "bot_7f3a1c",
                    "session_id": "3f6c1b9e-7f42-4d2a-9a6f-0d5b1f0b21c7",
                    "at": 1753600512004
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Прийнято. Будь-який `2xx`/`3xx` вважається успіхом і не повторюється."
          },
          "400": {
            "description": "Будь-який `4xx` — остаточна відмова: ми НЕ повторюємо доставку. Якщо хочете ретрай, відповідайте `5xx`."
          },
          "500": {
            "description": "Тимчасовий збій. Ми повторимо доставку тричі — через 1 с, 5 с і 25 с."
          }
        }
      }
    }
  }
}