# ВАЖНО (для будущих правок):
# — HTTP-методы (GET, POST, PUT, DELETE) — мировой стандарт, не переводимы
# — URL endpoint'ов (/public/v1/contacts) — технические идентификаторы
# — JSON-поля (contact_id, created_at) — программные идентификаторы
# — Статус-коды (200, 401, 403, 429) — мировой стандарт
# — Технические термины: Bearer, Token, JSON, Webhook, OAuth, JWT — устоявшиеся
# Переводятся: tags[].name, tags[].description, servers[].description, info.license.name,
#              summary/description endpoint'ов, ENUM-значения когда они для людей.
# UI-метки Redoc переводятся через public/js/lib/redoc-ru-i18n.js
# (Redoc 2.x не имеет встроенной локализации).

openapi: 3.0.3
info:
  title: BOTIX Public API
  description: |
    Публичный программный интерфейс платформы BOTIX (botix.pro) — позволяет интеграторам
    и сторонним разработчикам встраивать функционал BOTIX в свои продукты: магазины, CRM,
    мобильные приложения, бэкенды.

    **Авторизация:** заголовок `Authorization: Bearer btx_live_…`. Каждый ключ привязан
    к одному `project_id`. Каждый ключ имеет список `scopes` — операций, которые им
    разрешены. Без нужного scope — `403 INSUFFICIENT_SCOPE`.

    **Гейтинг по тарифу:**
    - тариф без `api_enabled` — `403 API_NOT_AVAILABLE_ON_PLAN`
    - триальный тариф (`api_write_enabled=0`) — read-only, POST/PUT/PATCH/DELETE → `403 TRIAL_READ_ONLY`
    - лимиты `api_rate_per_minute` / `api_rate_per_day` — из настроек тарифа (см. ЛК `/billing`)

    **Стандарт ответов:** обёртка `{success, data, meta}` для успеха и `{success:false, error:{code,message,details}}` для ошибки.

    **Версионирование:** `/public/v1/*` фиксирован. Новые поля в ответах могут добавляться, удаление/смена типов запрещены.

    ## Что нового в 1.1.0 (2026-05-22)

    - **Rate-limit заголовки** во всех ответах: `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`. На 429 — дополнительно `Retry-After`.
    - **Cursor-based пагинация** — query-параметр `cursor` + поле `meta.next_cursor` параллельно с существующим `page`/`per_page` (старые клиенты не ломаются).
    - **Bulk endpoints** для интеграторов: `POST /contacts/bulk-create`, `POST /messages/bulk-send`, `POST /contacts/bulk-update` (до 100 операций за запрос, частичный успех).

    Изменения обратно-совместимые: все endpoint v1.0 работают без правок на стороне клиента.
  version: "1.1.0"
  contact:
    name: BOTIX
    url: https://botix.pro
    email: hello@botix.pro
  license:
    name: Условия использования публичного API BOTIX
    url: https://botix.pro/legal

servers:
  - url: https://api.botix.pro
    description: Боевой сервер
  - url: http://localhost:8888
    description: Localhost (монолит) — для разработки

security:
  - bearerAuth: []

tags:
  - name: System
    description: Служебные эндпоинты
  - name: Contacts
    description: Контакты (CRM-карточка клиента)
  - name: Messages
    description: Сообщения (история и отправка)
  - name: Chats
    description: Чаты (диалоги bot↔контакт)
  - name: Scenarios
    description: Сценарии (визуальный конструктор)
  - name: Channels
    description: Каналы связи (Telegram, виджет, …)
  - name: Webhooks
    description: Подписки на события

paths:
  /public/v1/me:
    get:
      operationId: meGet
      x-codeSamples:
        - lang: 'curl'
          source: |
            curl -X GET 'https://api.botix.pro/public/v1/me' \
              -H 'Authorization: Bearer btx_live_<ваш-ключ>'
        - lang: 'PHP'
          label: 'PHP (botix-pro/sdk)'
          source: |
            <?php
            use BotixPro\Sdk\Client;
            
            $client = new Client('btx_live_<ваш-ключ>');
            $result = $client->system()->meGet();
            print_r($result);
        - lang: 'Python'
          label: 'Python (botix)'
          source: |
            import botix
            
            client = botix.Client('btx_live_<ваш-ключ>')
            result = client.system.me_get()
            print(result)
        - lang: 'JavaScript'
          label: 'Node.js (@botix/sdk)'
          source: |
            import { BotixClient } from '@botix/sdk';
            
            const client = new BotixClient('btx_live_<ваш-ключ>');
            const result = await client.system.meGet();
            console.log(result);
      tags: ["System"]
      summary: Контекст текущего ключа
      description: Возвращает project_id, scopes, plan_key, остаток rate-limit
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MeResponse'
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /public/v1/contacts:
    get:
      operationId: contactsList
      x-codeSamples:
        - lang: 'curl'
          source: |
            curl -X GET 'https://api.botix.pro/public/v1/contacts?page=1&per_page=50&tag=vip' \
              -H 'Authorization: Bearer btx_live_<ваш-ключ>'
        - lang: 'PHP'
          label: 'PHP (botix-pro/sdk)'
          source: |
            <?php
            use BotixPro\Sdk\Client;
            
            $client = new Client('btx_live_<ваш-ключ>');
            $result = $client->contacts()->contactsList(['page' => 1, 'per_page' => 50, 'tag' => 'vip']);
            print_r($result);
        - lang: 'Python'
          label: 'Python (botix)'
          source: |
            import botix
            
            client = botix.Client('btx_live_<ваш-ключ>')
            result = client.contacts.contacts_list(page=1, per_page=50, tag='vip')
            print(result)
        - lang: 'JavaScript'
          label: 'Node.js (@botix/sdk)'
          source: |
            import { BotixClient } from '@botix/sdk';
            
            const client = new BotixClient('btx_live_<ваш-ключ>');
            const result = await client.contacts.contactsList({ page: 1, per_page: 50, tag: 'vip' });
            console.log(result);
      tags: ["Contacts"]
      summary: Список контактов
      parameters:
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PerPage'
        - $ref: '#/components/parameters/Cursor'
        - $ref: '#/components/parameters/Limit'
        - in: query
          name: tag
          schema: { type: string }
          description: Фильтр по тегу (точное совпадение)
        - in: query
          name: channel
          schema: { type: string, enum: [telegram, whatsapp, vk, widget, email, phone] }
        - in: query
          name: lead_status
          schema: { type: string, enum: [new, warm, hot, client, lost] }
        - in: query
          name: since
          schema: { type: string, format: date-time }
          description: Фильтр по created_at >= since
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/SuccessWithMeta'
                  - properties:
                      data:
                        type: array
                        items: { $ref: '#/components/schemas/Contact' }
    post:
      operationId: contactsCreate
      x-codeSamples:
        - lang: 'curl'
          source: |
            curl -X POST 'https://api.botix.pro/public/v1/contacts' \
              -H 'Authorization: Bearer btx_live_<ваш-ключ>' \
              -H 'Content-Type: application/json' \
              -H "Idempotency-Key: $(uuidgen)" \
              -d '{
                "name": "Иван Иванов",
                "phone": "+79991234567",
                "channel": "telegram",
                "tags": [
                    "lead"
                ]
            }'
        - lang: 'PHP'
          label: 'PHP (botix-pro/sdk)'
          source: |
            <?php
            use BotixPro\Sdk\Client;
            
            $client = new Client('btx_live_<ваш-ключ>');
            $result = $client->contacts()->contactsCreate(['name' => 'Иван Иванов', 'phone' => '+79991234567', 'channel' => 'telegram', 'tags' => ['lead']]);
            print_r($result);
        - lang: 'Python'
          label: 'Python (botix)'
          source: |
            import botix
            
            client = botix.Client('btx_live_<ваш-ключ>')
            result = client.contacts.contacts_create(name='Иван Иванов', phone='+79991234567', channel='telegram', tags=['lead'])
            print(result)
        - lang: 'JavaScript'
          label: 'Node.js (@botix/sdk)'
          source: |
            import { BotixClient } from '@botix/sdk';
            
            const client = new BotixClient('btx_live_<ваш-ключ>');
            const result = await client.contacts.contactsCreate({ name: 'Иван Иванов', phone: '+79991234567', channel: 'telegram', tags: ['lead'] });
            console.log(result);
      tags: ["Contacts"]
      summary: Создать контакт
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ContactWritable' }
      responses:
        '201':
          description: Created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessContact'
        '400': { $ref: '#/components/responses/BadRequest' }

  /public/v1/contacts/{id}:
    parameters:
      - $ref: '#/components/parameters/IdPath'
    get:
      operationId: contactsGet
      x-codeSamples:
        - lang: 'curl'
          source: |
            curl -X GET 'https://api.botix.pro/public/v1/contacts/12345' \
              -H 'Authorization: Bearer btx_live_<ваш-ключ>'
        - lang: 'PHP'
          label: 'PHP (botix-pro/sdk)'
          source: |
            <?php
            use BotixPro\Sdk\Client;
            
            $client = new Client('btx_live_<ваш-ключ>');
            $result = $client->contacts()->contactsGet(12345);
            print_r($result);
        - lang: 'Python'
          label: 'Python (botix)'
          source: |
            import botix
            
            client = botix.Client('btx_live_<ваш-ключ>')
            result = client.contacts.contacts_get(12345)
            print(result)
        - lang: 'JavaScript'
          label: 'Node.js (@botix/sdk)'
          source: |
            import { BotixClient } from '@botix/sdk';
            
            const client = new BotixClient('btx_live_<ваш-ключ>');
            const result = await client.contacts.contactsGet(12345);
            console.log(result);
      tags: ["Contacts"]
      summary: Карточка контакта
      responses:
        '200':
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SuccessContact' }
          description: OK
        '404': { $ref: '#/components/responses/NotFound' }
    put:
      operationId: contactsUpdate
      x-codeSamples:
        - lang: 'curl'
          source: |
            curl -X PUT 'https://api.botix.pro/public/v1/contacts/12345' \
              -H 'Authorization: Bearer btx_live_<ваш-ключ>' \
              -H 'Content-Type: application/json' \
              -H "Idempotency-Key: $(uuidgen)" \
              -d '{
                "name": "Иван Петров",
                "lead_status": "warm"
            }'
        - lang: 'PHP'
          label: 'PHP (botix-pro/sdk)'
          source: |
            <?php
            use BotixPro\Sdk\Client;
            
            $client = new Client('btx_live_<ваш-ключ>');
            $result = $client->contacts()->contactsUpdate(12345, ['name' => 'Иван Петров', 'lead_status' => 'warm']);
            print_r($result);
        - lang: 'Python'
          label: 'Python (botix)'
          source: |
            import botix
            
            client = botix.Client('btx_live_<ваш-ключ>')
            result = client.contacts.contacts_update(12345, name='Иван Петров', lead_status='warm')
            print(result)
        - lang: 'JavaScript'
          label: 'Node.js (@botix/sdk)'
          source: |
            import { BotixClient } from '@botix/sdk';
            
            const client = new BotixClient('btx_live_<ваш-ключ>');
            const result = await client.contacts.contactsUpdate(12345, { name: 'Иван Петров', lead_status: 'warm' });
            console.log(result);
      tags: ["Contacts"]
      summary: Обновить контакт
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ContactWritable' }
      responses:
        '200':
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SuccessContact' }
          description: OK
        '404': { $ref: '#/components/responses/NotFound' }
    delete:
      operationId: contactsDelete
      x-codeSamples:
        - lang: 'curl'
          source: |
            curl -X DELETE 'https://api.botix.pro/public/v1/contacts/12345' \
              -H 'Authorization: Bearer btx_live_<ваш-ключ>' \
              -H "Idempotency-Key: $(uuidgen)"
        - lang: 'PHP'
          label: 'PHP (botix-pro/sdk)'
          source: |
            <?php
            use BotixPro\Sdk\Client;
            
            $client = new Client('btx_live_<ваш-ключ>');
            $result = $client->contacts()->contactsDelete(12345);
            print_r($result);
        - lang: 'Python'
          label: 'Python (botix)'
          source: |
            import botix
            
            client = botix.Client('btx_live_<ваш-ключ>')
            result = client.contacts.contacts_delete(12345)
            print(result)
        - lang: 'JavaScript'
          label: 'Node.js (@botix/sdk)'
          source: |
            import { BotixClient } from '@botix/sdk';
            
            const client = new BotixClient('btx_live_<ваш-ключ>');
            const result = await client.contacts.contactsDelete(12345);
            console.log(result);
      tags: ["Contacts"]
      summary: Удалить контакт (soft delete)
      responses:
        '204': { description: No Content }
        '404': { $ref: '#/components/responses/NotFound' }

  /public/v1/contacts/{id}/tags:
    parameters:
      - $ref: '#/components/parameters/IdPath'
    post:
      operationId: contactsAddTag
      x-codeSamples:
        - lang: 'curl'
          source: |
            curl -X POST 'https://api.botix.pro/public/v1/contacts/12345/tags' \
              -H 'Authorization: Bearer btx_live_<ваш-ключ>' \
              -H 'Content-Type: application/json' \
              -H "Idempotency-Key: $(uuidgen)" \
              -d '{
                "tag": "vip"
            }'
        - lang: 'PHP'
          label: 'PHP (botix-pro/sdk)'
          source: |
            <?php
            use BotixPro\Sdk\Client;
            
            $client = new Client('btx_live_<ваш-ключ>');
            $result = $client->contacts()->contactsAddTag(12345, ['tag' => 'vip']);
            print_r($result);
        - lang: 'Python'
          label: 'Python (botix)'
          source: |
            import botix
            
            client = botix.Client('btx_live_<ваш-ключ>')
            result = client.contacts.contacts_add_tag(12345, tag='vip')
            print(result)
        - lang: 'JavaScript'
          label: 'Node.js (@botix/sdk)'
          source: |
            import { BotixClient } from '@botix/sdk';
            
            const client = new BotixClient('btx_live_<ваш-ключ>');
            const result = await client.contacts.contactsAddTag(12345, { tag: 'vip' });
            console.log(result);
      tags: ["Contacts"]
      summary: Добавить тег контакту
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [tag]
              properties:
                tag: { type: string, example: "vip" }
      responses:
        '200':
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  data:
                    type: object
                    properties:
                      tags:
                        type: array
                        items: { type: string }
          description: OK

  /public/v1/contacts/{id}/tags/{tag}:
    parameters:
      - $ref: '#/components/parameters/IdPath'
      - in: path
        name: tag
        required: true
        schema: { type: string }
    delete:
      operationId: contactsRemoveTag
      x-codeSamples:
        - lang: 'curl'
          source: |
            curl -X DELETE 'https://api.botix.pro/public/v1/contacts/12345/tags/vip' \
              -H 'Authorization: Bearer btx_live_<ваш-ключ>' \
              -H "Idempotency-Key: $(uuidgen)"
        - lang: 'PHP'
          label: 'PHP (botix-pro/sdk)'
          source: |
            <?php
            use BotixPro\Sdk\Client;
            
            $client = new Client('btx_live_<ваш-ключ>');
            $result = $client->contacts()->contactsRemoveTag(12345, 'vip');
            print_r($result);
        - lang: 'Python'
          label: 'Python (botix)'
          source: |
            import botix
            
            client = botix.Client('btx_live_<ваш-ключ>')
            result = client.contacts.contacts_remove_tag(12345, 'vip')
            print(result)
        - lang: 'JavaScript'
          label: 'Node.js (@botix/sdk)'
          source: |
            import { BotixClient } from '@botix/sdk';
            
            const client = new BotixClient('btx_live_<ваш-ключ>');
            const result = await client.contacts.contactsRemoveTag(12345, 'vip');
            console.log(result);
      tags: ["Contacts"]
      summary: Снять тег с контакта
      responses:
        '200':
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  data:
                    type: object
                    properties:
                      tags:
                        type: array
                        items: { type: string }
          description: OK

  /public/v1/contacts/bulk-create:
    post:
      operationId: contactsBulkCreate
      x-codeSamples:
        - lang: 'curl'
          source: |
            curl -X POST 'https://api.botix.pro/public/v1/contacts/bulk-create' \
              -H 'Authorization: Bearer btx_live_<ваш-ключ>' \
              -H 'Content-Type: application/json' \
              -H "Idempotency-Key: $(uuidgen)" \
              -d '{
                "contacts": [
                    {
                        "name": "Иван",
                        "phone": "+79991234567",
                        "channel": "telegram"
                    },
                    {
                        "name": "Мария",
                        "phone": "+79997654321",
                        "channel": "whatsapp"
                    }
                ]
            }'
        - lang: 'PHP'
          label: 'PHP (botix-pro/sdk)'
          source: |
            <?php
            use BotixPro\Sdk\Client;
            
            $client = new Client('btx_live_<ваш-ключ>');
            $result = $client->contacts()->contactsBulkCreate(['contacts' => [['name' => 'Иван', 'phone' => '+79991234567', 'channel' => 'telegram'], ['name' => 'Мария', 'phone' => '+79997654321', 'channel' => 'whatsapp']]]);
            print_r($result);
        - lang: 'Python'
          label: 'Python (botix)'
          source: |
            import botix
            
            client = botix.Client('btx_live_<ваш-ключ>')
            result = client.contacts.contacts_bulk_create(contacts=[{'name': 'Иван', 'phone': '+79991234567', 'channel': 'telegram'}, {'name': 'Мария', 'phone': '+79997654321', 'channel': 'whatsapp'}])
            print(result)
        - lang: 'JavaScript'
          label: 'Node.js (@botix/sdk)'
          source: |
            import { BotixClient } from '@botix/sdk';
            
            const client = new BotixClient('btx_live_<ваш-ключ>');
            const result = await client.contacts.contactsBulkCreate({ contacts: [{ name: 'Иван', phone: '+79991234567', channel: 'telegram' }, { name: 'Мария', phone: '+79997654321', channel: 'whatsapp' }] });
            console.log(result);
      tags: ["Contacts"]
      summary: Массовое создание контактов (v1.1)
      description: |
        Создаёт до 100 контактов одним запросом. Каждый item — в своей мини-транзакции,
        частичный успех допустим (см. поля `created`/`failed`/`results`).

        Bulk-операция в rate-limit считается как **1 запрос** (а не 100) — это сознательное
        послабление для поощрения bulk-вызовов.

        Поддерживает `Idempotency-Key` (24ч).
      parameters:
        - in: header
          name: Idempotency-Key
          required: false
          schema: { type: string, maxLength: 255 }
          description: |
            Уникальный токен запроса для защиты от дублей при сетевых сбоях. Рекомендация —
            UUID v4 на каждую логическую операцию. **TTL ключа — 24 часа.** После истечения
            тот же `Idempotency-Key` создаст новую операцию, кешированный результат не
            возвращается. Повтор в окне 24 часов вернёт сохранённый ответ + заголовок
            `Idempotent-Replayed: 1`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [contacts]
              properties:
                contacts:
                  type: array
                  minItems: 1
                  maxItems: 100
                  items: { $ref: '#/components/schemas/ContactWritable' }
      responses:
        '200':
          description: Результат массового создания (включая частичный успех)
          content:
            application/json:
              schema: { $ref: '#/components/schemas/BulkResult' }
        '400': { $ref: '#/components/responses/BadRequest' }

  /public/v1/contacts/bulk-update:
    post:
      operationId: contactsBulkUpdate
      x-codeSamples:
        - lang: 'curl'
          source: |
            curl -X POST 'https://api.botix.pro/public/v1/contacts/bulk-update' \
              -H 'Authorization: Bearer btx_live_<ваш-ключ>' \
              -H 'Content-Type: application/json' \
              -H "Idempotency-Key: $(uuidgen)" \
              -d '{
                "updates": [
                    {
                        "id": 12345,
                        "fields": {
                            "lead_status": "hot"
                        }
                    },
                    {
                        "id": 12346,
                        "fields": {
                            "lead_status": "lost"
                        }
                    }
                ]
            }'
        - lang: 'PHP'
          label: 'PHP (botix-pro/sdk)'
          source: |
            <?php
            use BotixPro\Sdk\Client;
            
            $client = new Client('btx_live_<ваш-ключ>');
            $result = $client->contacts()->contactsBulkUpdate(['updates' => [['id' => 12345, 'fields' => ['lead_status' => 'hot']], ['id' => 12346, 'fields' => ['lead_status' => 'lost']]]]);
            print_r($result);
        - lang: 'Python'
          label: 'Python (botix)'
          source: |
            import botix
            
            client = botix.Client('btx_live_<ваш-ключ>')
            result = client.contacts.contacts_bulk_update(updates=[{'id': 12345, 'fields': {'lead_status': 'hot'}}, {'id': 12346, 'fields': {'lead_status': 'lost'}}])
            print(result)
        - lang: 'JavaScript'
          label: 'Node.js (@botix/sdk)'
          source: |
            import { BotixClient } from '@botix/sdk';
            
            const client = new BotixClient('btx_live_<ваш-ключ>');
            const result = await client.contacts.contactsBulkUpdate({ updates: [{ id: 12345, fields: { lead_status: 'hot' } }, { id: 12346, fields: { lead_status: 'lost' } }] });
            console.log(result);
      tags: ["Contacts"]
      summary: Массовое обновление контактов (v1.1)
      description: |
        Обновляет до 100 контактов одним запросом. Каждый item — `{id, fields:{...}}`.
        Частичный успех допустим. Поддерживает `Idempotency-Key`.
      parameters:
        - in: header
          name: Idempotency-Key
          required: false
          schema: { type: string, maxLength: 255 }
          description: |
            Уникальный токен запроса для защиты от дублей при сетевых сбоях. Рекомендация —
            UUID v4 на каждую логическую операцию. **TTL ключа — 24 часа.** После истечения
            тот же `Idempotency-Key` создаст новую операцию, кешированный результат не
            возвращается. Повтор в окне 24 часов вернёт сохранённый ответ + заголовок
            `Idempotent-Replayed: 1`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [updates]
              properties:
                updates:
                  type: array
                  minItems: 1
                  maxItems: 100
                  items:
                    type: object
                    required: [id, fields]
                    properties:
                      id: { type: integer }
                      fields: { $ref: '#/components/schemas/ContactWritable' }
      responses:
        '200':
          description: Результат массового обновления
          content:
            application/json:
              schema: { $ref: '#/components/schemas/BulkResult' }
        '400': { $ref: '#/components/responses/BadRequest' }

  /public/v1/messages/bulk-send:
    post:
      operationId: messagesBulkSend
      x-codeSamples:
        - lang: 'curl'
          source: |
            curl -X POST 'https://api.botix.pro/public/v1/messages/bulk-send' \
              -H 'Authorization: Bearer btx_live_<ваш-ключ>' \
              -H 'Content-Type: application/json' \
              -H "Idempotency-Key: $(uuidgen)" \
              -d '{
                "messages": [
                    {
                        "contact_id": 12345,
                        "channel": "telegram",
                        "content": "Привет!"
                    },
                    {
                        "contact_id": 12346,
                        "channel": "whatsapp",
                        "content": "Здравствуйте!"
                    }
                ]
            }'
        - lang: 'PHP'
          label: 'PHP (botix-pro/sdk)'
          source: |
            <?php
            use BotixPro\Sdk\Client;
            
            $client = new Client('btx_live_<ваш-ключ>');
            $result = $client->messages()->messagesBulkSend(['messages' => [['contact_id' => 12345, 'channel' => 'telegram', 'content' => 'Привет!'], ['contact_id' => 12346, 'channel' => 'whatsapp', 'content' => 'Здравствуйте!']]]);
            print_r($result);
        - lang: 'Python'
          label: 'Python (botix)'
          source: |
            import botix
            
            client = botix.Client('btx_live_<ваш-ключ>')
            result = client.messages.messages_bulk_send(messages=[{'contact_id': 12345, 'channel': 'telegram', 'content': 'Привет!'}, {'contact_id': 12346, 'channel': 'whatsapp', 'content': 'Здравствуйте!'}])
            print(result)
        - lang: 'JavaScript'
          label: 'Node.js (@botix/sdk)'
          source: |
            import { BotixClient } from '@botix/sdk';
            
            const client = new BotixClient('btx_live_<ваш-ключ>');
            const result = await client.messages.messagesBulkSend({ messages: [{ contact_id: 12345, channel: 'telegram', content: 'Привет!' }, { contact_id: 12346, channel: 'whatsapp', content: 'Здравствуйте!' }] });
            console.log(result);
      tags: ["Messages"]
      summary: Массовая отправка сообщений (v1.1)
      description: |
        Отправляет до 50 сообщений одним запросом. Каждое — независимо: частичный
        успех допустим. Результат каждого item — либо `message_id` (успех), либо `error`.

        Per-channel rate-limit (Telegram 30 msg/sec и т.п.) **не обходится** — если канал
        отвергает сообщение из-за своего лимита, item помечается `DELIVERY_FAILED`.

        Поддерживает `Idempotency-Key`.
      parameters:
        - in: header
          name: Idempotency-Key
          required: false
          schema: { type: string, maxLength: 255 }
          description: |
            Уникальный токен запроса для защиты от дублей при сетевых сбоях. Рекомендация —
            UUID v4 на каждую логическую операцию. **TTL ключа — 24 часа.** После истечения
            тот же `Idempotency-Key` создаст новую операцию, кешированный результат не
            возвращается. Повтор в окне 24 часов вернёт сохранённый ответ + заголовок
            `Idempotent-Replayed: 1`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [messages]
              properties:
                messages:
                  type: array
                  minItems: 1
                  maxItems: 50
                  items:
                    type: object
                    required: [contact_id, content]
                    properties:
                      contact_id: { type: integer }
                      channel: { type: string }
                      content: { type: string }
                      attachments:
                        type: array
                        items:
                          type: object
                          properties:
                            type: { type: string }
                            url: { type: string, format: uri }
                            name: { type: string }
      responses:
        '200':
          description: Результат массовой отправки
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  data:
                    type: object
                    properties:
                      sent: { type: integer }
                      failed: { type: integer }
                      results:
                        type: array
                        items:
                          type: object
                          properties:
                            index: { type: integer }
                            message_id: { type: integer, nullable: true }
                            conversation_id: { type: integer, nullable: true }
                            channel: { type: string, nullable: true }
                            error: { type: string, nullable: true }
                            message: { type: string, nullable: true }
        '400': { $ref: '#/components/responses/BadRequest' }

  /public/v1/messages:
    get:
      operationId: messagesList
      x-codeSamples:
        - lang: 'curl'
          source: |
            curl -X GET 'https://api.botix.pro/public/v1/messages?page=1&per_page=50&contact_id=12345' \
              -H 'Authorization: Bearer btx_live_<ваш-ключ>'
        - lang: 'PHP'
          label: 'PHP (botix-pro/sdk)'
          source: |
            <?php
            use BotixPro\Sdk\Client;
            
            $client = new Client('btx_live_<ваш-ключ>');
            $result = $client->messages()->messagesList(['page' => 1, 'per_page' => 50, 'contact_id' => 12345]);
            print_r($result);
        - lang: 'Python'
          label: 'Python (botix)'
          source: |
            import botix
            
            client = botix.Client('btx_live_<ваш-ключ>')
            result = client.messages.messages_list(page=1, per_page=50, contact_id=12345)
            print(result)
        - lang: 'JavaScript'
          label: 'Node.js (@botix/sdk)'
          source: |
            import { BotixClient } from '@botix/sdk';
            
            const client = new BotixClient('btx_live_<ваш-ключ>');
            const result = await client.messages.messagesList({ page: 1, per_page: 50, contact_id: 12345 });
            console.log(result);
      tags: ["Messages"]
      summary: История сообщений
      parameters:
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PerPage'
        - $ref: '#/components/parameters/Cursor'
        - $ref: '#/components/parameters/Limit'
        - { in: query, name: contact_id, schema: { type: integer } }
        - { in: query, name: chat_id, schema: { type: integer }, description: "alias для conversation_id" }
        - { in: query, name: channel, schema: { type: string } }
        - { in: query, name: role, schema: { type: string, enum: [user, bot, operator, system] } }
        - { in: query, name: since, schema: { type: string, format: date-time } }
      responses:
        '200':
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/SuccessWithMeta'
                  - properties:
                      data:
                        type: array
                        items: { $ref: '#/components/schemas/Message' }
          description: OK
    post:
      operationId: messagesSend
      x-codeSamples:
        - lang: 'curl'
          source: |
            curl -X POST 'https://api.botix.pro/public/v1/messages' \
              -H 'Authorization: Bearer btx_live_<ваш-ключ>' \
              -H 'Content-Type: application/json' \
              -H "Idempotency-Key: $(uuidgen)" \
              -d '{
                "contact_id": 12345,
                "channel": "telegram",
                "content": "Здравствуйте! Ваш заказ #1024 готов."
            }'
        - lang: 'PHP'
          label: 'PHP (botix-pro/sdk)'
          source: |
            <?php
            use BotixPro\Sdk\Client;
            
            $client = new Client('btx_live_<ваш-ключ>');
            $result = $client->messages()->messagesSend(['contact_id' => 12345, 'channel' => 'telegram', 'content' => 'Здравствуйте! Ваш заказ #1024 готов.']);
            print_r($result);
        - lang: 'Python'
          label: 'Python (botix)'
          source: |
            import botix
            
            client = botix.Client('btx_live_<ваш-ключ>')
            result = client.messages.messages_send(contact_id=12345, channel='telegram', content='Здравствуйте! Ваш заказ #1024 готов.')
            print(result)
        - lang: 'JavaScript'
          label: 'Node.js (@botix/sdk)'
          source: |
            import { BotixClient } from '@botix/sdk';
            
            const client = new BotixClient('btx_live_<ваш-ключ>');
            const result = await client.messages.messagesSend({ contact_id: 12345, channel: 'telegram', content: 'Здравствуйте! Ваш заказ #1024 готов.' });
            console.log(result);
      tags: ["Messages"]
      summary: Отправить сообщение
      description: |
        Реально отправляет сообщение в канал (Telegram/widget/VK) и фиксирует его
        в истории. Канал выбирается так:
        1. явный `channel` в payload (если передан);
        2. `last_channel` контакта;
        3. иначе 422 `NO_CHANNEL_AVAILABLE`.

        Поддерживается заголовок `Idempotency-Key` (стандарт Stripe): повторный
        запрос с тем же ключом в течение 24 часов вернёт сохранённый ответ без
        новой отправки. Ответ будет содержать заголовок `Idempotent-Replayed: 1`.
      parameters:
        - in: header
          name: Idempotency-Key
          required: false
          schema: { type: string, maxLength: 255 }
          description: |
            Уникальный токен запроса для защиты от дублей при сетевых сбоях. Рекомендация —
            UUID v4 на каждую логическую операцию. **TTL ключа — 24 часа.** После истечения
            тот же `Idempotency-Key` создаст новую операцию, кешированный результат не
            возвращается. Повтор в окне 24 часов вернёт сохранённый ответ + заголовок
            `Idempotent-Replayed: 1`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [contact_id, content]
              properties:
                contact_id: { type: integer }
                channel:
                  type: string
                  description: "telegram | widget | vk. Опционально — иначе берётся last_channel контакта."
                content: { type: string }
                attachments:
                  type: array
                  items:
                    type: object
                    properties:
                      type: { type: string, example: "image" }
                      url: { type: string, format: uri }
                      name: { type: string }
      responses:
        '200':
          description: Сообщение успешно отправлено каналом
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  data:
                    type: object
                    properties:
                      id: { type: integer }
                      message_id: { type: integer }
                      conversation_id: { type: integer }
                      contact_id: { type: integer }
                      channel: { type: string }
                      status: { type: string, example: "sent" }
        '422':
          description: |
            Не удалось отправить из-за состояния (контакт/канал). Возможные коды:
            `NO_CHANNEL_AVAILABLE`, `CHANNEL_NOT_SUPPORTED_YET`, `CHANNEL_NOT_ALLOWED`,
            `CHANNEL_NOT_CONNECTED`, `CONTACT_NOT_REACHABLE`.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
        '502':
          description: Канал ответил ошибкой при отправке (`DELIVERY_FAILED`)
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }

  /public/v1/scenarios:
    get:
      operationId: scenariosList
      x-codeSamples:
        - lang: 'curl'
          source: |
            curl -X GET 'https://api.botix.pro/public/v1/scenarios' \
              -H 'Authorization: Bearer btx_live_<ваш-ключ>'
        - lang: 'PHP'
          label: 'PHP (botix-pro/sdk)'
          source: |
            <?php
            use BotixPro\Sdk\Client;
            
            $client = new Client('btx_live_<ваш-ключ>');
            $result = $client->scenarios()->scenariosList();
            print_r($result);
        - lang: 'Python'
          label: 'Python (botix)'
          source: |
            import botix
            
            client = botix.Client('btx_live_<ваш-ключ>')
            result = client.scenarios.scenarios_list()
            print(result)
        - lang: 'JavaScript'
          label: 'Node.js (@botix/sdk)'
          source: |
            import { BotixClient } from '@botix/sdk';
            
            const client = new BotixClient('btx_live_<ваш-ключ>');
            const result = await client.scenarios.scenariosList();
            console.log(result);
      tags: ["Scenarios"]
      summary: Список сценариев проекта
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/SuccessWithMeta'
                  - properties:
                      data:
                        type: array
                        items: { $ref: '#/components/schemas/Scenario' }

  /public/v1/scenarios/{id}/run:
    parameters:
      - $ref: '#/components/parameters/IdPath'
    post:
      operationId: scenariosRun
      x-codeSamples:
        - lang: 'curl'
          source: |
            curl -X POST 'https://api.botix.pro/public/v1/scenarios/12345/run' \
              -H 'Authorization: Bearer btx_live_<ваш-ключ>' \
              -H 'Content-Type: application/json' \
              -H "Idempotency-Key: $(uuidgen)" \
              -d '{
                "contact_id": 12345,
                "channel": "widget",
                "variables": {
                    "utm_source": "kwork"
                }
            }'
        - lang: 'PHP'
          label: 'PHP (botix-pro/sdk)'
          source: |
            <?php
            use BotixPro\Sdk\Client;
            
            $client = new Client('btx_live_<ваш-ключ>');
            $result = $client->scenarios()->scenariosRun(12345, ['contact_id' => 12345, 'channel' => 'widget', 'variables' => ['utm_source' => 'kwork']]);
            print_r($result);
        - lang: 'Python'
          label: 'Python (botix)'
          source: |
            import botix
            
            client = botix.Client('btx_live_<ваш-ключ>')
            result = client.scenarios.scenarios_run(12345, contact_id=12345, channel='widget', variables={'utm_source': 'kwork'})
            print(result)
        - lang: 'JavaScript'
          label: 'Node.js (@botix/sdk)'
          source: |
            import { BotixClient } from '@botix/sdk';
            
            const client = new BotixClient('btx_live_<ваш-ключ>');
            const result = await client.scenarios.scenariosRun(12345, { contact_id: 12345, channel: 'widget', variables: { utm_source: 'kwork' } });
            console.log(result);
      tags: ["Scenarios"]
      summary: Запустить сценарий для контакта
      description: |
        Запускает сценарий синхронно: создаёт conversation, отправляет первый блок
        в канал, возвращает 200 OK c результатом. Для widget-канала первый блок
        возвращается прямо в ответе (`first_block`/`first_blocks`); для остальных
        каналов он уходит сразу пользователю, в ответе остаётся пустым.

        Поддерживает `Idempotency-Key` (24ч). Если контакт сейчас в другом сценарии —
        вернётся 422 `CONTACT_BUSY`; передайте `force=true`, чтобы прервать предыдущий
        диалог.
      parameters:
        - in: header
          name: Idempotency-Key
          required: false
          schema: { type: string, maxLength: 255 }
          description: |
            Уникальный токен запроса для защиты от дублей при сетевых сбоях. Рекомендация —
            UUID v4 на каждую логическую операцию. **TTL ключа — 24 часа.** После истечения
            тот же `Idempotency-Key` создаст новую операцию, кешированный результат не
            возвращается. Повтор в окне 24 часов вернёт сохранённый ответ + заголовок
            `Idempotent-Replayed: 1`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [contact_id]
              properties:
                contact_id: { type: integer }
                channel:
                  type: string
                  description: "telegram | widget | vk. Опционально — иначе берётся last_channel контакта."
                variables:
                  type: object
                  additionalProperties: true
                force:
                  type: boolean
                  default: false
                  description: Принудительно прервать активный сценарий контакта.
      responses:
        '200':
          description: Сценарий запущен
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  data:
                    type: object
                    properties:
                      conversation_id: { type: integer }
                      scenario_id: { type: integer }
                      channel: { type: string }
                      status: { type: string, example: "started" }
                      first_block:
                        type: object
                        nullable: true
                        description: Первый рендер сценария (widget-канал — заполнено; tg/vk — пусто).
                      first_blocks:
                        type: array
                        items:
                          type: object
        '422':
          description: |
            Возможные коды: `SCENARIO_INACTIVE`, `CONTACT_NOT_FOUND`, `NO_CHANNEL_AVAILABLE`,
            `CHANNEL_NOT_SUPPORTED_YET`, `CHANNEL_NOT_ALLOWED`, `CHANNEL_NOT_CONNECTED`,
            `CONTACT_NOT_REACHABLE`, `CONTACT_BUSY`, `NO_START_BLOCK`.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
        '404':
          $ref: '#/components/responses/NotFound'

  /public/v1/chats:
    get:
      operationId: chatsList
      x-codeSamples:
        - lang: 'curl'
          source: |
            curl -X GET 'https://api.botix.pro/public/v1/chats?page=1&per_page=50&status=active' \
              -H 'Authorization: Bearer btx_live_<ваш-ключ>'
        - lang: 'PHP'
          label: 'PHP (botix-pro/sdk)'
          source: |
            <?php
            use BotixPro\Sdk\Client;
            
            $client = new Client('btx_live_<ваш-ключ>');
            $result = $client->chats()->chatsList(['page' => 1, 'per_page' => 50, 'status' => 'active']);
            print_r($result);
        - lang: 'Python'
          label: 'Python (botix)'
          source: |
            import botix
            
            client = botix.Client('btx_live_<ваш-ключ>')
            result = client.chats.chats_list(page=1, per_page=50, status='active')
            print(result)
        - lang: 'JavaScript'
          label: 'Node.js (@botix/sdk)'
          source: |
            import { BotixClient } from '@botix/sdk';
            
            const client = new BotixClient('btx_live_<ваш-ключ>');
            const result = await client.chats.chatsList({ page: 1, per_page: 50, status: 'active' });
            console.log(result);
      tags: ["Chats"]
      summary: Список чатов
      parameters:
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PerPage'
        - $ref: '#/components/parameters/Cursor'
        - $ref: '#/components/parameters/Limit'
        - { in: query, name: status, schema: { type: string, enum: [active, paused, completed, operator] } }
        - { in: query, name: channel, schema: { type: string } }
        - { in: query, name: contact_id, schema: { type: integer } }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/SuccessWithMeta'
                  - properties:
                      data:
                        type: array
                        items: { $ref: '#/components/schemas/Chat' }

  /public/v1/chats/{id}/messages:
    parameters:
      - $ref: '#/components/parameters/IdPath'
    get:
      operationId: chatsMessages
      x-codeSamples:
        - lang: 'curl'
          source: |
            curl -X GET 'https://api.botix.pro/public/v1/chats/12345/messages?page=1&per_page=50' \
              -H 'Authorization: Bearer btx_live_<ваш-ключ>'
        - lang: 'PHP'
          label: 'PHP (botix-pro/sdk)'
          source: |
            <?php
            use BotixPro\Sdk\Client;
            
            $client = new Client('btx_live_<ваш-ключ>');
            $result = $client->chats()->chatsMessages(12345, ['page' => 1, 'per_page' => 50]);
            print_r($result);
        - lang: 'Python'
          label: 'Python (botix)'
          source: |
            import botix
            
            client = botix.Client('btx_live_<ваш-ключ>')
            result = client.chats.chats_messages(12345, page=1, per_page=50)
            print(result)
        - lang: 'JavaScript'
          label: 'Node.js (@botix/sdk)'
          source: |
            import { BotixClient } from '@botix/sdk';
            
            const client = new BotixClient('btx_live_<ваш-ключ>');
            const result = await client.chats.chatsMessages(12345, { page: 1, per_page: 50 });
            console.log(result);
      tags: ["Chats"]
      summary: Сообщения внутри чата
      parameters:
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PerPage'
        - $ref: '#/components/parameters/Cursor'
        - $ref: '#/components/parameters/Limit'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/SuccessWithMeta'
                  - properties:
                      data:
                        type: array
                        items: { $ref: '#/components/schemas/Message' }
        '404': { $ref: '#/components/responses/NotFound' }

  /public/v1/channels:
    get:
      operationId: channelsList
      x-codeSamples:
        - lang: 'curl'
          source: |
            curl -X GET 'https://api.botix.pro/public/v1/channels' \
              -H 'Authorization: Bearer btx_live_<ваш-ключ>'
        - lang: 'PHP'
          label: 'PHP (botix-pro/sdk)'
          source: |
            <?php
            use BotixPro\Sdk\Client;
            
            $client = new Client('btx_live_<ваш-ключ>');
            $result = $client->channels()->channelsList();
            print_r($result);
        - lang: 'Python'
          label: 'Python (botix)'
          source: |
            import botix
            
            client = botix.Client('btx_live_<ваш-ключ>')
            result = client.channels.channels_list()
            print(result)
        - lang: 'JavaScript'
          label: 'Node.js (@botix/sdk)'
          source: |
            import { BotixClient } from '@botix/sdk';
            
            const client = new BotixClient('btx_live_<ваш-ключ>');
            const result = await client.channels.channelsList();
            console.log(result);
      tags: ["Channels"]
      summary: Каналы проекта
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/Channel' }
                  meta:
                    type: object
                    properties:
                      total: { type: integer }

  /public/v1/webhooks:
    get:
      operationId: webhooksList
      x-codeSamples:
        - lang: 'curl'
          source: |
            curl -X GET 'https://api.botix.pro/public/v1/webhooks' \
              -H 'Authorization: Bearer btx_live_<ваш-ключ>'
        - lang: 'PHP'
          label: 'PHP (botix-pro/sdk)'
          source: |
            <?php
            use BotixPro\Sdk\Client;
            
            $client = new Client('btx_live_<ваш-ключ>');
            $result = $client->webhooks()->webhooksList();
            print_r($result);
        - lang: 'Python'
          label: 'Python (botix)'
          source: |
            import botix
            
            client = botix.Client('btx_live_<ваш-ключ>')
            result = client.webhooks.webhooks_list()
            print(result)
        - lang: 'JavaScript'
          label: 'Node.js (@botix/sdk)'
          source: |
            import { BotixClient } from '@botix/sdk';
            
            const client = new BotixClient('btx_live_<ваш-ключ>');
            const result = await client.webhooks.webhooksList();
            console.log(result);
      tags: [Webhooks]
      summary: Список подписок
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/Webhook' }
    post:
      operationId: webhooksCreate
      x-codeSamples:
        - lang: 'curl'
          source: |
            curl -X POST 'https://api.botix.pro/public/v1/webhooks' \
              -H 'Authorization: Bearer btx_live_<ваш-ключ>' \
              -H 'Content-Type: application/json' \
              -H "Idempotency-Key: $(uuidgen)" \
              -d '{
                "url": "https:\/\/example.com\/botix-webhook",
                "events": [
                    "message.received",
                    "chat.created"
                ]
            }'
        - lang: 'PHP'
          label: 'PHP (botix-pro/sdk)'
          source: |
            <?php
            use BotixPro\Sdk\Client;
            
            $client = new Client('btx_live_<ваш-ключ>');
            $result = $client->webhooks()->webhooksCreate(['url' => 'https://example.com/botix-webhook', 'events' => ['message.received', 'chat.created']]);
            print_r($result);
        - lang: 'Python'
          label: 'Python (botix)'
          source: |
            import botix
            
            client = botix.Client('btx_live_<ваш-ключ>')
            result = client.webhooks.webhooks_create(url='https://example.com/botix-webhook', events=['message.received', 'chat.created'])
            print(result)
        - lang: 'JavaScript'
          label: 'Node.js (@botix/sdk)'
          source: |
            import { BotixClient } from '@botix/sdk';
            
            const client = new BotixClient('btx_live_<ваш-ключ>');
            const result = await client.webhooks.webhooksCreate({ url: 'https://example.com/botix-webhook', events: ['message.received', 'chat.created'] });
            console.log(result);
      tags: [Webhooks]
      summary: Создать подписку
      description: |
        После создания возвращается ОДИН РАЗ поле `data.secret` — для верификации
        заголовка `X-Botix-Signature` (HMAC-SHA256). Сохраните его на своей стороне.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url, events]
              properties:
                url: { type: string, format: uri, example: "https://example.com/botix-webhook" }
                events:
                  type: array
                  items: { $ref: '#/components/schemas/WebhookEvent' }
      responses:
        '201':
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  data:
                    type: object
                    properties:
                      webhook: { $ref: '#/components/schemas/Webhook' }
                      secret: { type: string, description: "Секрет для HMAC. Показывается один раз!" }

  /public/v1/webhooks/{id}:
    parameters:
      - $ref: '#/components/parameters/IdPath'
    put:
      operationId: webhooksUpdate
      x-codeSamples:
        - lang: 'curl'
          source: |
            curl -X PUT 'https://api.botix.pro/public/v1/webhooks/12345' \
              -H 'Authorization: Bearer btx_live_<ваш-ключ>' \
              -H 'Content-Type: application/json' \
              -H "Idempotency-Key: $(uuidgen)" \
              -d '{
                "url": "https:\/\/example.com\/botix-webhook-v2",
                "status": "active"
            }'
        - lang: 'PHP'
          label: 'PHP (botix-pro/sdk)'
          source: |
            <?php
            use BotixPro\Sdk\Client;
            
            $client = new Client('btx_live_<ваш-ключ>');
            $result = $client->webhooks()->webhooksUpdate(12345, ['url' => 'https://example.com/botix-webhook-v2', 'status' => 'active']);
            print_r($result);
        - lang: 'Python'
          label: 'Python (botix)'
          source: |
            import botix
            
            client = botix.Client('btx_live_<ваш-ключ>')
            result = client.webhooks.webhooks_update(12345, url='https://example.com/botix-webhook-v2', status='active')
            print(result)
        - lang: 'JavaScript'
          label: 'Node.js (@botix/sdk)'
          source: |
            import { BotixClient } from '@botix/sdk';
            
            const client = new BotixClient('btx_live_<ваш-ключ>');
            const result = await client.webhooks.webhooksUpdate(12345, { url: 'https://example.com/botix-webhook-v2', status: 'active' });
            console.log(result);
      tags: [Webhooks]
      summary: Обновить подписку
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                url: { type: string, format: uri }
                events:
                  type: array
                  items: { $ref: '#/components/schemas/WebhookEvent' }
                status: { type: string, enum: [active, paused] }
      responses:
        '200': { description: OK }
        '404': { $ref: '#/components/responses/NotFound' }
    delete:
      operationId: webhooksDelete
      x-codeSamples:
        - lang: 'curl'
          source: |
            curl -X DELETE 'https://api.botix.pro/public/v1/webhooks/12345' \
              -H 'Authorization: Bearer btx_live_<ваш-ключ>' \
              -H "Idempotency-Key: $(uuidgen)"
        - lang: 'PHP'
          label: 'PHP (botix-pro/sdk)'
          source: |
            <?php
            use BotixPro\Sdk\Client;
            
            $client = new Client('btx_live_<ваш-ключ>');
            $result = $client->webhooks()->webhooksDelete(12345);
            print_r($result);
        - lang: 'Python'
          label: 'Python (botix)'
          source: |
            import botix
            
            client = botix.Client('btx_live_<ваш-ключ>')
            result = client.webhooks.webhooks_delete(12345)
            print(result)
        - lang: 'JavaScript'
          label: 'Node.js (@botix/sdk)'
          source: |
            import { BotixClient } from '@botix/sdk';
            
            const client = new BotixClient('btx_live_<ваш-ключ>');
            const result = await client.webhooks.webhooksDelete(12345);
            console.log(result);
      tags: [Webhooks]
      summary: Удалить подписку
      responses:
        '204': { description: No Content }
        '404': { $ref: '#/components/responses/NotFound' }

  /public/v1/webhooks/{id}/test:
    parameters:
      - $ref: '#/components/parameters/IdPath'
    post:
      operationId: webhooksTest
      x-codeSamples:
        - lang: 'curl'
          source: |
            curl -X POST 'https://api.botix.pro/public/v1/webhooks/12345/test' \
              -H 'Authorization: Bearer btx_live_<ваш-ключ>' \
              -H 'Content-Type: application/json' \
              -H "Idempotency-Key: $(uuidgen)"
        - lang: 'PHP'
          label: 'PHP (botix-pro/sdk)'
          source: |
            <?php
            use BotixPro\Sdk\Client;
            
            $client = new Client('btx_live_<ваш-ключ>');
            $result = $client->webhooks()->webhooksTest(12345);
            print_r($result);
        - lang: 'Python'
          label: 'Python (botix)'
          source: |
            import botix
            
            client = botix.Client('btx_live_<ваш-ключ>')
            result = client.webhooks.webhooks_test(12345)
            print(result)
        - lang: 'JavaScript'
          label: 'Node.js (@botix/sdk)'
          source: |
            import { BotixClient } from '@botix/sdk';
            
            const client = new BotixClient('btx_live_<ваш-ключ>');
            const result = await client.webhooks.webhooksTest(12345);
            console.log(result);
      tags: [Webhooks]
      summary: Тестовая отправка
      description: |
        Шлёт фиктивное событие `test` на URL подписки. Удобно для проверки HMAC
        и доступности endpoint'а на стороне клиента.
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  data:
                    type: object
                    properties:
                      delivered: { type: boolean }
                      response_code: { type: integer, nullable: true }
                      response_ms: { type: integer, nullable: true }
                      error: { type: string, nullable: true }
                      request_id: { type: string, format: uuid }

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: "btx_live_<40-char-alnum>"

  parameters:
    IdPath:
      in: path
      name: id
      required: true
      schema: { type: integer }
    Page:
      in: query
      name: page
      schema: { type: integer, minimum: 1, default: 1 }
    PerPage:
      in: query
      name: per_page
      schema: { type: integer, minimum: 1, maximum: 100, default: 50 }
    Cursor:
      in: query
      name: cursor
      required: false
      schema: { type: string }
      description: |
        v1.1+. Opaque cursor для постраничной выборки. Если передан — параметры `page`/`per_page` игнорируются.
        Получите следующий cursor из `meta.next_cursor` предыдущего ответа.
        `null`/отсутствие `next_cursor` = последняя страница.
    Limit:
      in: query
      name: limit
      required: false
      schema: { type: integer, minimum: 1, maximum: 100, default: 50 }
      description: |
        v1.1+. Размер cursor-страницы. Используется только в cursor-режиме
        (`?cursor=...`); для классической page-пагинации применяется `per_page`.

  headers:
    XRateLimitLimit:
      description: Лимит запросов за минуту по текущему тарифу клиента
      schema: { type: integer }
    XRateLimitRemaining:
      description: Сколько ещё запросов разрешено в текущем минутном окне
      schema: { type: integer }
    XRateLimitReset:
      description: Unix timestamp следующего обнуления минутного окна
      schema: { type: integer }
    RetryAfter:
      description: Секунды до возможности повтора (только при 429)
      schema: { type: integer }

  responses:
    Unauthorized:
      description: Ключ отсутствует или невалиден
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    Forbidden:
      description: |
        Недостаточно scope, тариф без API, или режим read-only:
        `INSUFFICIENT_SCOPE` / `API_NOT_AVAILABLE_ON_PLAN` / `TRIAL_READ_ONLY`
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    NotFound:
      description: Ресурс не найден или не принадлежит project_id
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    BadRequest:
      description: Ошибка валидации / некорректный JSON
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    RateLimited:
      description: Превышен лимит (минутный или суточный)
      headers:
        Retry-After: { $ref: '#/components/headers/RetryAfter' }
        X-RateLimit-Limit: { $ref: '#/components/headers/XRateLimitLimit' }
        X-RateLimit-Remaining: { $ref: '#/components/headers/XRateLimitRemaining' }
        X-RateLimit-Reset: { $ref: '#/components/headers/XRateLimitReset' }
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }

  schemas:
    SuccessWithMeta:
      type: object
      properties:
        success: { type: boolean, example: true }
        data: {}
        meta:
          type: object
          properties:
            page: { type: integer, description: "Только в page-режиме" }
            per_page: { type: integer, description: "Только в page-режиме" }
            total: { type: integer, description: "Только в page-режиме" }
            next_cursor:
              type: string
              nullable: true
              description: "v1.1+. В cursor-режиме — токен для следующей страницы. null = последняя страница. В page-режиме отсутствует."
            has_more:
              type: boolean
              description: "v1.1+. true если есть ещё страницы (cursor-режим)."

    Error:
      type: object
      properties:
        success: { type: boolean, example: false }
        error:
          type: object
          properties:
            code:
              type: string
              enum:
                - MISSING_API_KEY
                - INVALID_API_KEY
                - KEY_REVOKED
                - IP_BANNED
                - INSUFFICIENT_SCOPE
                - API_NOT_AVAILABLE_ON_PLAN
                - TRIAL_READ_ONLY
                - RATE_LIMIT_EXCEEDED
                - VALIDATION_ERROR
                - INVALID_JSON
                - INVALID_CURSOR
                - NOT_FOUND
                - NO_CHANNEL_AVAILABLE
                - CHANNEL_NOT_SUPPORTED_YET
                - CHANNEL_NOT_ALLOWED
                - CHANNEL_NOT_CONNECTED
                - CONTACT_NOT_REACHABLE
                - CONTACT_NOT_FOUND
                - CONTACT_BUSY
                - SCENARIO_NOT_FOUND
                - SCENARIO_INACTIVE
                - NO_START_BLOCK
                - DELIVERY_FAILED
            message: { type: string }
            details: { type: object, additionalProperties: true }

    MeResponse:
      type: object
      properties:
        success: { type: boolean }
        data:
          type: object
          properties:
            project_id: { type: integer }
            api_key_id: { type: integer }
            scopes:
              type: array
              items: { type: string }
            plan_key: { type: string }
            limits:
              type: object
              properties:
                api_enabled: { type: boolean }
                api_write_enabled: { type: boolean }
                per_minute: { type: integer }
                per_day: { type: integer }
            rate_limit:
              type: object
              properties:
                limit: { type: integer }
                remaining: { type: integer }
                reset_at: { type: string, format: date-time }

    SuccessContact:
      type: object
      properties:
        success: { type: boolean }
        data: { $ref: '#/components/schemas/Contact' }

    BulkResult:
      type: object
      description: |
        Универсальный ответ bulk-эндпоинтов. Поле `created`/`updated` зависит от операции.
        В `results` — массив элементов с индексом и либо `id` (успех), либо `error` (отказ).
      properties:
        success: { type: boolean }
        data:
          type: object
          properties:
            created: { type: integer, nullable: true, description: "Для bulk-create" }
            updated: { type: integer, nullable: true, description: "Для bulk-update" }
            failed: { type: integer }
            results:
              type: array
              items:
                type: object
                properties:
                  index: { type: integer }
                  id: { type: integer, nullable: true }
                  error: { type: string, nullable: true, example: "DUPLICATE_KEY" }
                  message: { type: string, nullable: true }

    Contact:
      type: object
      properties:
        id: { type: integer }
        project_id: { type: integer }
        first_name: { type: string, nullable: true }
        last_name: { type: string, nullable: true }
        phone: { type: string, nullable: true }
        email: { type: string, nullable: true }
        telegram_user_id: { type: integer, nullable: true }
        telegram_username: { type: string, nullable: true }
        whatsapp_phone: { type: string, nullable: true }
        vk_user_id: { type: integer, nullable: true }
        avatar_url: { type: string, nullable: true }
        city: { type: string, nullable: true }
        timezone: { type: string, nullable: true }
        language: { type: string, nullable: true, example: "ru" }
        birthday: { type: string, format: date, nullable: true }
        tags:
          type: array
          items: { type: string }
        lead_status:
          type: string
          enum: [new, warm, hot, client, lost]
        messages_count: { type: integer }
        last_message_at: { type: string, format: date-time, nullable: true }
        last_channel:
          type: string
          nullable: true
          description: Последний канал, через который контакт писал (telegram/vk/widget/...)
        first_message_at: { type: string, format: date-time, nullable: true }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time, nullable: true }
        widget_session_id:
          type: string
          nullable: true
          description: Идентификатор сессии в виджете (для канала widget)

    ContactWritable:
      type: object
      properties:
        first_name: { type: string }
        last_name: { type: string }
        phone: { type: string }
        email: { type: string }
        telegram_user_id: { type: integer }
        telegram_username: { type: string }
        whatsapp_phone: { type: string }
        vk_user_id: { type: integer }
        avatar_url: { type: string }
        city: { type: string }
        timezone: { type: string }
        language: { type: string }
        birthday: { type: string, format: date }
        tags:
          type: array
          items: { type: string }
        lead_status:
          type: string
          enum: [new, warm, hot, client, lost]

    Message:
      type: object
      properties:
        id: { type: integer }
        project_id: { type: integer }
        conversation_id: { type: integer }
        contact_id: { type: integer }
        role: { type: string, enum: [user, bot, operator, system] }
        content: { type: string }
        attachments:
          type: array
          nullable: true
          items:
            type: object
            additionalProperties: true
        source: { type: string, nullable: true, example: "scenario" }
        scenario_id: { type: integer, nullable: true }
        block_id: { type: string, nullable: true }
        channel: { type: string }
        is_read: { type: integer, enum: [0, 1] }
        created_at: { type: string, format: date-time }

    Chat:
      type: object
      properties:
        id: { type: integer }
        project_id: { type: integer }
        contact_id: { type: integer }
        channel: { type: string }
        scenario_id: { type: integer, nullable: true }
        status: { type: string, enum: [active, paused, completed, operator] }
        messages_count: { type: integer }
        last_message_at: { type: string, format: date-time, nullable: true }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time, nullable: true }

    Scenario:
      type: object
      properties:
        id: { type: integer }
        name: { type: string }
        description: { type: string }
        is_active: { type: boolean }
        trigger_type: { type: string }
        trigger_value: { type: string, nullable: true }
        channels:
          type: array
          items: { type: string }
        version: { type: integer }
        published_at: { type: string, format: date-time, nullable: true }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time, nullable: true }

    Channel:
      type: object
      properties:
        type: { type: string, example: "telegram" }
        label: { type: string, example: "Telegram" }
        allowed: { type: boolean, description: "Канал доступен на текущем тарифе" }
        connected: { type: boolean, description: "Канал фактически подключён" }
        connected_at: { type: string, format: date-time, nullable: true }

    Webhook:
      type: object
      properties:
        id: { type: integer }
        project_id: { type: integer }
        api_key_id: { type: integer }
        url: { type: string, format: uri }
        events:
          type: array
          items: { $ref: '#/components/schemas/WebhookEvent' }
        status: { type: string, enum: [active, paused, failed] }
        fail_count: { type: integer }
        last_delivered_at: { type: string, format: date-time, nullable: true }
        created_at: { type: string, format: date-time }

    WebhookEvent:
      type: string
      enum:
        - contact.created
        - contact.updated
        - contact.deleted
        - contact.tagged
        - message.received
        - message.sent
        - chat.opened
        - chat.closed
        - chat.assigned
        - scenario.started
        - scenario.completed
        - scenario.failed
