openapi: 3.1.0
info:
  title: Climails · Публичный API
  version: "1.0.0"
  description: |
    Climails — платформа email- и мультиканальных рассылок. Этот API даёт
    программный доступ ко всему, что есть в кабинете: отправка писем и
    сообщений, контакты, списки и сегменты, шаблоны, кампании, события,
    автоматизации, отправляющие домены, веб-хуки и аналитика.

    **Как читать эту страницу.** Слева — все эндпоинты, сгруппированные по
    разделам. Кликните на операцию, чтобы увидеть её параметры, тело запроса
    и примеры ответов. Кнопка **TRY** выполняет запрос прямо из браузера —
    вставьте свой API-ключ в поле авторизации (см. «Аутентификация» ниже).

    ## Нужны только письма (регистрация, сброс пароля)?
    Не нужно поднимать свой почтовый сервер и не нужно разбираться в контактах,
    списках, сегментах, кампаниях и автоматизациях — это всё для маркетинговых
    рассылок по базе. Для транзакционных писем со своим шаблоном достаточно
    трёх шагов:

    1. **Подтвердите свой домен** — раздел **domains**: добавьте домен и
       пропишите выданные DNS-записи (SPF/DKIM/DMARC). Без этого письма либо
       не уйдут, либо попадут в спам. Делается один раз.
    2. **Создайте шаблон** (по желанию) — раздел **templates**: `POST /templates`
       с вашим HTML и плейсхолдерами вроде `{{name}}` / `{{reset_link}}`.
       В ответ придёт `template_id`. Можно и без шаблона — слать `html` прямо
       в запросе.
    3. **Отправляйте письма** — `POST /messages`, по одному на каждое событие
       (регистрация, сброс пароля и т.п.).

    Отправка по своему шаблону с подстановкой переменных:

    ```bash
    curl -X POST https://api.climails.com/v1/messages \
      -H "Authorization: Bearer sb_live_<prefix>.<secret>" \
      -H "Content-Type: application/json" \
      -d '{
        "channel": "email",
        "to": "user@example.com",
        "from_email": "no-reply@ваш-домен.com",
        "template_id": "tmpl_reset_password",
        "variables": { "name": "Иван", "reset_link": "https://ваш-сайт.com/r/abc" }
      }'
    ```

    Всё остальное в этой доке (contacts, lists, segments, campaigns,
    automations, events, webhooks) — опционально и вам сейчас не требуется.

    ## Аутентификация
    Все запросы авторизуются Bearer API-ключом:
    `Authorization: Bearer sb_live_<prefix>.<secret>`

    Ключи создаются в кабинете: **Настройки → API-ключи**. Каждый ключ
    привязан к одному рабочему пространству и имеет набор скоупов (например
    `contacts:write`, `messages:send`). Ключ показывается полностью только один
    раз при создании.

    ## Быстрый старт
    Отправить письмо одним запросом:

    ```bash
    curl -X POST https://api.climails.com/v1/messages \
      -H "Authorization: Bearer sb_live_<prefix>.<secret>" \
      -H "Content-Type: application/json" \
      -d '{
        "channel": "email",
        "to": "user@example.com",
        "from_email": "hello@ваш-домен.com",
        "subject": "Привет от Climails",
        "html": "<h1>Готово</h1>"
      }'
    ```

    В ответ придёт `202` с `message_id` и `status: queued`. Статус доставки
    затем можно получить через `GET /messages/{id}` или ловить веб-хуками.
    Обязательные поля — только `channel` и `to`; `from_email` должен
    принадлежать верифицированному отправляющему домену.

    ## Скоупы
    Доступ каждого ключа ограничен его скоупами — сервер проверяет их на каждом
    запросе и возвращает `403 { "error": "scope required: ..." }`, если скоупа
    нет. Скоуп `*` даёт полный доступ. Право `write` включает в себя `read`
    для того же ресурса.

    | Скоуп | Что открывает |
    |-------|----------------|
    | `contacts:read`    | Чтение контактов, списков, сегментов, кастомных полей |
    | `contacts:write`   | Изменение контактов/списков/сегментов, импорт, suppression |
    | `templates:read`   | Чтение шаблонов и их версий |
    | `templates:write`  | Создание/изменение/удаление/импорт шаблонов |
    | `campaigns:read`   | Чтение кампаний и статистики |
    | `campaigns:write`  | Создание, изменение, запуск/пауза/отмена кампаний |
    | `automations:read` | Чтение автоматизаций |
    | `automations:write`| Создание/изменение/активация/триггер автоматизаций |
    | `messages:send`    | Транзакционная отправка и чтение статуса сообщения |
    | `events:write`     | Приём product-событий |
    | `webhooks:read`    | Чтение веб-хуков и доставок |
    | `webhooks:write`   | Управление веб-хуками, тест, реплей доставок |
    | `domains:read`     | Чтение sending-доменов и DNS-записей |
    | `domains:write`    | Добавление/верификация доменов, авто-DNS, IP-пулы |
    | `analytics:read`   | Аналитика по кампаниям и воркспейсу |
    | `pii:read`         | Email/phone в открытом виде (иначе маскируются) |

    ## Рабочее пространство
    Ресурсы, специфичные для рабочего пространства (контакты, сегменты,
    шаблоны, кампании, автоматизации, домены, веб-хуки), монтируются под
    `/workspaces/{workspace_id}/...`. Узнать `workspace_id` можно через
    `GET /workspaces`. API-ключ авторизует операции **только** в своём
    рабочем пространстве — запрос с чужим `workspace_id` вернёт `403`.

    Транзакционная отправка (`POST /messages`) и приём событий
    (`POST /events`) воркспейс из ключа берут автоматически — путь не
    параметризуется.

    ## Идемпотентность
    Мутирующие запросы принимают заголовок `Idempotency-Key`
    (до 64 символов). Повтор с тем же ключом в течение 24 часов вернёт
    исходный результат, не создавая дубль.

    ## Базовый URL
    Production: `https://api.climails.com/v1`
  contact:
    name: Climails
    email: api@climails.com
  license:
    name: Proprietary

servers:
  - url: https://api.climails.com/v1
    description: Production
  - url: https://api.staging.climails.com/v1
    description: Staging
  - url: http://localhost:8080/api/v1
    description: Local dev

security:
  - bearerAuth: []

tags:
  - name: workspaces
    description: "Рабочие пространства. `workspace_id` отсюда подставляется в пути остальных запросов; у API-ключа он всегда один."
  - name: messages
    description: "Отправка одиночных писем и сообщений: подтверждение регистрации, сброс пароля, чеки, коды. Главный раздел для транзакционной почты."
  - name: templates
    description: "Шаблоны писем. Создайте шаблон один раз и отправляйте по `template_id`, подставляя переменные — не нужно хранить HTML в коде."
  - name: domains
    description: "Отправляющие домены. Подтвердите свой домен (SPF/DKIM/DMARC), чтобы письма доходили и не попадали в спам. Обязательный шаг перед отправкой."
  - name: contacts
    description: "Контакты — люди в вашей базе. Нужны для рассылок по базе; для разовых писем на конкретный адрес не обязательны."
  - name: lists
    description: "Списки — статичные группы контактов (например «Подписчики»). Часть маркетинговых рассылок, для транзакционных писем не нужны."
  - name: segments
    description: "Сегменты — динамические выборки контактов по условиям. Для транзакционных писем не нужны."
  - name: campaigns
    description: "Массовые рассылки по спискам и сегментам (маркетинг). Для транзакционных писем не используются."
  - name: automations
    description: "Сценарии «событие → письмо» (welcome-серии, брошенная корзина). Опционально, для маркетинга — для писем регистрации/сброса не нужны."
  - name: events
    description: "Приём событий из вашего продукта — триггеры для автоматизаций и аналитики. Нужны, только если используете automations."
  - name: suppressions
    description: "Список подавления — адреса, которым нельзя писать (отписки, жалобы, жёсткие баунсы). Climails учитывает его автоматически."
  - name: webhooks
    description: "Веб-хуки — Climails сам уведомляет ваш сервер о доставке, открытии, отписке. Удобно, чтобы отслеживать судьбу письма."

paths:
  /health:
    get:
      summary: Проверка доступности
      security: []
      responses:
        "200": { description: ok }

  /workspaces:
    get:
      tags: [workspaces]
      summary: Рабочие пространства, доступные ключу
      responses:
        "200":
          description: ok
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/Workspace' }
    post:
      tags: [workspaces]
      summary: Создать новое рабочее пространство
      description: |
        Создаёт новый workspace и назначает текущего пользователя его владельцем.
        Требует интерактивную сессию (пользовательский JWT из кабинета) — API-ключ
        привязан к одному workspace и создавать новые не может.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name: { type: string, maxLength: 100, description: "Название рабочего пространства." }
      responses:
        "201":
          description: created
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Workspace' }

  /messages:
    post:
      tags: [messages]
      summary: Отправить транзакционное сообщение
      description: |
        Отправляет одиночное сообщение (сервисное письмо, SMS, push, telegram,
        whatsapp, viber). Рабочее пространство берётся из API-ключа — указывать
        его не нужно. Можно отправить по шаблону (`template_id` + `variables`)
        либо передать `subject`/`html`/`text` напрямую.

        Требует скоуп `messages:send`.
      x-required-scopes: [messages:send]
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/MessageRequest' }
      responses:
        "202":
          description: queued
          content:
            application/json:
              schema:
                type: object
                properties:
                  message_id: { type: string }
                  status: { type: string, example: queued }
                  existing:
                    type: boolean
                    description: "true, если результат вернулся по идемпотентному повтору"
        "401": { $ref: '#/components/responses/Unauthorized' }
        "403": { $ref: '#/components/responses/Forbidden' }
        "422": { $ref: '#/components/responses/Invalid' }

  /messages/{id}:
    parameters:
      - { name: id, in: path, required: true, schema: { type: string } }
    get:
      tags: [messages]
      summary: Статус сообщения и события доставки
      responses:
        "200":
          description: ok
          content:
            application/json:
              schema: { $ref: '#/components/schemas/MessageStatus' }

  /events:
    post:
      tags: [events]
      summary: Принять product-событие для автоматизаций и аналитики
      description: "Требует скоуп `events:write`."
      x-required-scopes: [events:write]
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ProductEvent' }
      responses: { "202": { description: accepted } }

  /events/bulk:
    post:
      tags: [events]
      summary: Принять до 1000 событий одним запросом
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [events]
              properties:
                events:
                  type: array
                  maxItems: 1000
                  items: { $ref: '#/components/schemas/ProductEvent' }
      responses: { "202": { description: accepted } }

  /workspaces/{workspace_id}/contacts:
    parameters:
      - $ref: '#/components/parameters/WorkspaceId'
    get:
      tags: [contacts]
      summary: Список контактов
      parameters:
        - { name: cursor, in: query, schema: { type: string } }
        - { name: limit, in: query, schema: { type: integer, default: 50, maximum: 500 } }
        - { name: list_id, in: query, schema: { type: string } }
        - { name: segment_id, in: query, schema: { type: string } }
        - { name: query, in: query, schema: { type: string }, description: "поиск по email/телефону/имени" }
      responses:
        "200":
          description: ok
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ContactPage' }
    post:
      tags: [contacts]
      summary: Создать или обновить контакт
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ContactUpsert' }
      responses:
        "200": { description: updated, content: { application/json: { schema: { $ref: '#/components/schemas/Contact' } } } }
        "201": { description: created, content: { application/json: { schema: { $ref: '#/components/schemas/Contact' } } } }

  /workspaces/{workspace_id}/contacts/bulk:
    parameters:
      - $ref: '#/components/parameters/WorkspaceId'
    post:
      tags: [contacts]
      summary: Создать/обновить до 1000 контактов одним запросом
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [contacts]
              properties:
                contacts:
                  type: array
                  maxItems: 1000
                  items: { $ref: '#/components/schemas/ContactUpsert' }
      responses:
        "200": { description: ok }

  /workspaces/{workspace_id}/contacts/import:
    parameters:
      - $ref: '#/components/parameters/WorkspaceId'
    post:
      tags: [contacts]
      summary: Запустить импорт CSV (возвращает ссылку для загрузки)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [filename, size_bytes, sha256]
              properties:
                filename: { type: string }
                size_bytes: { type: integer }
                sha256: { type: string }
                mapping: { type: object }
                dedup_keys:
                  type: array
                  items: { type: string, enum: [email, phone, external_id] }
      responses:
        "200":
          description: ok
          content:
            application/json:
              schema:
                type: object
                properties:
                  import_id: { type: string }
                  upload_url: { type: string, format: uri }

  /workspaces/{workspace_id}/contacts/{id}:
    parameters:
      - $ref: '#/components/parameters/WorkspaceId'
      - { name: id, in: path, required: true, schema: { type: string } }
    get:
      tags: [contacts]
      summary: Получить контакт
      responses:
        "200": { description: ok, content: { application/json: { schema: { $ref: '#/components/schemas/Contact' } } } }
    patch:
      tags: [contacts]
      summary: Обновить контакт
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ContactPatch' }
      responses:
        "200": { description: ok, content: { application/json: { schema: { $ref: '#/components/schemas/Contact' } } } }
    delete:
      tags: [contacts]
      summary: Удалить контакт
      responses:
        "204": { description: deleted }

  /workspaces/{workspace_id}/lists:
    parameters:
      - $ref: '#/components/parameters/WorkspaceId'
    get:
      tags: [lists]
      summary: Список списков
      responses: { "200": { description: ok } }
    post:
      tags: [lists]
      summary: Создать список
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name: { type: string }
      responses: { "201": { description: created } }

  /workspaces/{workspace_id}/segments:
    parameters:
      - $ref: '#/components/parameters/WorkspaceId'
    get:
      tags: [segments]
      summary: Список сегментов
      responses: { "200": { description: ok } }
    post:
      tags: [segments]
      summary: Создать сегмент
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/Segment' }
      responses: { "201": { description: created } }

  /workspaces/{workspace_id}/segments/preview:
    parameters:
      - $ref: '#/components/parameters/WorkspaceId'
    post:
      tags: [segments]
      summary: Оценить размер сегмента и выборку до сохранения
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/Segment' }
      responses:
        "200":
          description: ok
          content:
            application/json:
              schema:
                type: object
                properties:
                  estimated_count: { type: integer }
                  sample:
                    type: array
                    items: { $ref: '#/components/schemas/Contact' }

  /workspaces/{workspace_id}/segments/{id}/recalculate:
    parameters:
      - $ref: '#/components/parameters/WorkspaceId'
      - { name: id, in: path, required: true, schema: { type: string } }
    post:
      tags: [segments]
      summary: Пересчитать состав сегмента сейчас
      responses: { "202": { description: recalculating } }

  /workspaces/{workspace_id}/templates:
    parameters:
      - $ref: '#/components/parameters/WorkspaceId'
    get:
      tags: [templates]
      summary: Список шаблонов
      description: "Требует скоуп `templates:read`."
      responses: { "200": { description: ok } }
    post:
      tags: [templates]
      summary: Создать шаблон
      description: "Требует скоуп `templates:write`."
      x-required-scopes: [templates:write]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/Template' }
      responses: { "201": { description: created } }

  /workspaces/{workspace_id}/templates/{id}:
    parameters:
      - $ref: '#/components/parameters/WorkspaceId'
      - { name: id, in: path, required: true, schema: { type: string } }
    get:
      tags: [templates]
      summary: Получить шаблон
      description: "Требует скоуп `templates:read`."
      responses:
        "200": { description: ok, content: { application/json: { schema: { $ref: '#/components/schemas/Template' } } } }
    patch:
      tags: [templates]
      summary: Обновить шаблон
      description: "Требует скоуп `templates:write`."
      x-required-scopes: [templates:write]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/Template' }
      responses: { "200": { description: updated } }
    delete:
      tags: [templates]
      summary: Удалить шаблон
      description: "Требует скоуп `templates:write`."
      x-required-scopes: [templates:write]
      responses: { "204": { description: deleted } }

  /workspaces/{workspace_id}/templates/{id}/versions:
    parameters:
      - $ref: '#/components/parameters/WorkspaceId'
      - { name: id, in: path, required: true, schema: { type: string } }
    get:
      tags: [templates]
      summary: Список версий шаблона
      description: "Требует скоуп `templates:read`."
      responses: { "200": { description: ok } }

  /workspaces/{workspace_id}/templates/{id}/revert/{version}:
    parameters:
      - $ref: '#/components/parameters/WorkspaceId'
      - { name: id, in: path, required: true, schema: { type: string } }
      - { name: version, in: path, required: true, schema: { type: integer } }
    post:
      tags: [templates]
      summary: Откатить шаблон к предыдущей версии
      description: "Требует скоуп `templates:write`."
      x-required-scopes: [templates:write]
      responses: { "200": { description: reverted } }

  /workspaces/{workspace_id}/templates/import-html:
    parameters:
      - $ref: '#/components/parameters/WorkspaceId'
    post:
      tags: [templates]
      summary: Создать шаблон из готового HTML
      description: "Требует скоуп `templates:write`."
      x-required-scopes: [templates:write]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, html]
              properties:
                name: { type: string }
                subject: { type: string }
                html: { type: string }
      responses: { "201": { description: created } }

  /workspaces/{workspace_id}/templates/import-url:
    parameters:
      - $ref: '#/components/parameters/WorkspaceId'
    post:
      tags: [templates]
      summary: Создать шаблон, импортировав HTML по URL
      description: "Требует скоуп `templates:write`."
      x-required-scopes: [templates:write]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, url]
              properties:
                name: { type: string }
                url: { type: string, format: uri }
      responses: { "201": { description: created } }

  /workspaces/{workspace_id}/templates/{id}/preview:
    parameters:
      - $ref: '#/components/parameters/WorkspaceId'
      - { name: id, in: path, required: true, schema: { type: string } }
    post:
      tags: [templates]
      summary: Предпросмотр шаблона
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                variables: { type: object }
                contact_id: { type: string }
      responses:
        "200":
          description: ok
          content:
            application/json:
              schema:
                type: object
                properties:
                  subject: { type: string }
                  html: { type: string }
                  text: { type: string }

  /workspaces/{workspace_id}/campaigns:
    parameters:
      - $ref: '#/components/parameters/WorkspaceId'
    get:
      tags: [campaigns]
      summary: Список кампаний
      responses: { "200": { description: ok } }
    post:
      tags: [campaigns]
      summary: Создать кампанию (рассылка по спискам/сегментам)
      description: "Требует скоуп `campaigns:write`."
      x-required-scopes: [campaigns:write]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/Campaign' }
      responses: { "201": { description: created } }

  /workspaces/{workspace_id}/campaigns/preview-audience:
    parameters:
      - $ref: '#/components/parameters/WorkspaceId'
    post:
      tags: [campaigns]
      summary: Оценить аудиторию до сохранения кампании
      description: "Требует скоуп `campaigns:read` или `campaigns:write`."
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/Campaign' }
      responses:
        "200":
          description: ok
          content:
            application/json:
              schema:
                type: object
                properties:
                  estimated_count: { type: integer }

  /workspaces/{workspace_id}/campaigns/{id}:
    parameters:
      - $ref: '#/components/parameters/WorkspaceId'
      - { name: id, in: path, required: true, schema: { type: string } }
    get:
      tags: [campaigns]
      summary: Получить кампанию
      description: "Требует скоуп `campaigns:read`."
      responses:
        "200": { description: ok, content: { application/json: { schema: { $ref: '#/components/schemas/Campaign' } } } }
    patch:
      tags: [campaigns]
      summary: Обновить черновик кампании
      description: "Требует скоуп `campaigns:write`."
      x-required-scopes: [campaigns:write]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/Campaign' }
      responses: { "200": { description: updated } }

  /workspaces/{workspace_id}/campaigns/{id}/schedule:
    parameters:
      - $ref: '#/components/parameters/WorkspaceId'
      - { name: id, in: path, required: true, schema: { type: string } }
    post:
      tags: [campaigns]
      summary: Запланировать или отправить кампанию сейчас
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                at: { type: string, format: date-time }
                send_now: { type: boolean, default: false }
                by_timezone: { type: boolean, default: false }
      responses: { "202": { description: scheduled } }

  /workspaces/{workspace_id}/campaigns/{id}/pause:
    parameters:
      - $ref: '#/components/parameters/WorkspaceId'
      - { name: id, in: path, required: true, schema: { type: string } }
    post:
      tags: [campaigns]
      summary: Поставить кампанию на паузу
      responses: { "202": { description: paused } }

  /workspaces/{workspace_id}/campaigns/{id}/cancel:
    parameters:
      - $ref: '#/components/parameters/WorkspaceId'
      - { name: id, in: path, required: true, schema: { type: string } }
    post:
      tags: [campaigns]
      summary: Отменить кампанию
      responses: { "202": { description: cancelled } }

  /workspaces/{workspace_id}/campaigns/{id}/stats:
    parameters:
      - $ref: '#/components/parameters/WorkspaceId'
      - { name: id, in: path, required: true, schema: { type: string } }
    get:
      tags: [campaigns]
      summary: Статистика кампании
      responses:
        "200":
          description: ok
          content:
            application/json:
              schema: { $ref: '#/components/schemas/CampaignStats' }

  /workspaces/{workspace_id}/automations:
    parameters:
      - $ref: '#/components/parameters/WorkspaceId'
    get:
      tags: [automations]
      summary: Список автоматизаций
      responses: { "200": { description: ok } }
    post:
      tags: [automations]
      summary: Создать автоматизацию
      description: "Требует скоуп `automations:write`."
      x-required-scopes: [automations:write]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/Automation' }
      responses: { "201": { description: created } }

  /workspaces/{workspace_id}/automations/{id}:
    parameters:
      - $ref: '#/components/parameters/WorkspaceId'
      - { name: id, in: path, required: true, schema: { type: string } }
    get:
      tags: [automations]
      summary: Получить автоматизацию
      description: "Требует скоуп `automations:read`."
      responses:
        "200": { description: ok, content: { application/json: { schema: { $ref: '#/components/schemas/Automation' } } } }
    patch:
      tags: [automations]
      summary: Обновить автоматизацию
      description: "Требует скоуп `automations:write`."
      x-required-scopes: [automations:write]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/Automation' }
      responses: { "200": { description: updated } }

  /workspaces/{workspace_id}/automations/{id}/pause:
    parameters:
      - $ref: '#/components/parameters/WorkspaceId'
      - { name: id, in: path, required: true, schema: { type: string } }
    post:
      tags: [automations]
      summary: Поставить автоматизацию на паузу
      description: "Требует скоуп `automations:write`."
      x-required-scopes: [automations:write]
      responses: { "202": { description: paused } }

  /workspaces/{workspace_id}/automations/{id}/activate:
    parameters:
      - $ref: '#/components/parameters/WorkspaceId'
      - { name: id, in: path, required: true, schema: { type: string } }
    post:
      tags: [automations]
      summary: Активировать автоматизацию
      responses: { "202": { description: activated } }

  /workspaces/{workspace_id}/automations/{id}/trigger:
    parameters:
      - $ref: '#/components/parameters/WorkspaceId'
      - { name: id, in: path, required: true, schema: { type: string } }
    post:
      tags: [automations]
      summary: Вручную добавить контакт в автоматизацию
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                contact_id: { type: string }
                external_id: { type: string }
      responses: { "202": { description: triggered } }

  /workspaces/{workspace_id}/suppressions:
    parameters:
      - $ref: '#/components/parameters/WorkspaceId'
    post:
      tags: [suppressions]
      summary: Добавить адрес в список подавления
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [channel, address, reason]
              properties:
                channel: { type: string, enum: [email, sms, push, telegram, whatsapp, viber] }
                address: { type: string }
                reason: { type: string }
      responses: { "201": { description: created } }

  /workspaces/{workspace_id}/sending-domains:
    parameters:
      - $ref: '#/components/parameters/WorkspaceId'
    get:
      tags: [domains]
      summary: Список отправляющих доменов
      responses: { "200": { description: ok } }
    post:
      tags: [domains]
      summary: Добавить отправляющий домен
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [domain]
              properties:
                domain: { type: string }
                tracking_subdomain: { type: string }
      responses:
        "201":
          description: created — возвращаются DNS-записи для настройки
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SendingDomain' }

  /workspaces/{workspace_id}/sending-domains/{id}/verify:
    parameters:
      - $ref: '#/components/parameters/WorkspaceId'
      - { name: id, in: path, required: true, schema: { type: string } }
    post:
      tags: [domains]
      summary: Перепроверить DNS и подтвердить домен
      responses:
        "200":
          description: ok
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SendingDomain' }

  /workspaces/{workspace_id}/sending-domains/{id}/records:
    parameters:
      - $ref: '#/components/parameters/WorkspaceId'
      - { name: id, in: path, required: true, schema: { type: string } }
    get:
      tags: [domains]
      summary: DNS-записи (SPF/DKIM/DMARC) для публикации
      responses:
        "200":
          description: ok
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SendingDomain' }

  /workspaces/{workspace_id}/webhooks:
    parameters:
      - $ref: '#/components/parameters/WorkspaceId'
    get:
      tags: [webhooks]
      summary: Список веб-хуков
      description: "Требует скоуп `webhooks:read`."
      responses: { "200": { description: ok } }
    post:
      tags: [webhooks]
      summary: Создать веб-хук
      description: "Требует скоуп `webhooks:write`."
      x-required-scopes: [webhooks:write]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/Webhook' }
      responses: { "201": { description: created } }

  /workspaces/{workspace_id}/webhooks/{id}:
    parameters:
      - $ref: '#/components/parameters/WorkspaceId'
      - { name: id, in: path, required: true, schema: { type: string } }
    get:
      tags: [webhooks]
      summary: Получить веб-хук
      description: "Требует скоуп `webhooks:read`."
      responses: { "200": { description: ok } }
    delete:
      tags: [webhooks]
      summary: Удалить веб-хук
      description: "Требует скоуп `webhooks:write`."
      x-required-scopes: [webhooks:write]
      responses: { "204": { description: deleted } }

  /workspaces/{workspace_id}/webhooks/{id}/pause:
    parameters:
      - $ref: '#/components/parameters/WorkspaceId'
      - { name: id, in: path, required: true, schema: { type: string } }
    post:
      tags: [webhooks]
      summary: Приостановить доставки веб-хука
      description: "Требует скоуп `webhooks:write`."
      x-required-scopes: [webhooks:write]
      responses: { "202": { description: paused } }

  /workspaces/{workspace_id}/webhooks/{id}/resume:
    parameters:
      - $ref: '#/components/parameters/WorkspaceId'
      - { name: id, in: path, required: true, schema: { type: string } }
    post:
      tags: [webhooks]
      summary: Возобновить веб-хук
      description: "Требует скоуп `webhooks:write`."
      x-required-scopes: [webhooks:write]
      responses: { "202": { description: resumed } }

  /workspaces/{workspace_id}/webhooks/{id}/test:
    parameters:
      - $ref: '#/components/parameters/WorkspaceId'
      - { name: id, in: path, required: true, schema: { type: string } }
    post:
      tags: [webhooks]
      summary: Отправить тестовое событие на веб-хук
      description: "Требует скоуп `webhooks:write`."
      x-required-scopes: [webhooks:write]
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                event_type: { type: string }
                payload: { type: object }
      responses: { "200": { description: sent } }

  /workspaces/{workspace_id}/webhooks/{id}/headers:
    parameters:
      - $ref: '#/components/parameters/WorkspaceId'
      - { name: id, in: path, required: true, schema: { type: string } }
    patch:
      tags: [webhooks]
      summary: Обновить кастомные заголовки веб-хука
      description: "Требует скоуп `webhooks:write`."
      x-required-scopes: [webhooks:write]
      requestBody:
        required: true
        content:
          application/json:
            schema: { type: object, properties: { headers: { type: object } } }
      responses: { "200": { description: updated } }

  /workspaces/{workspace_id}/webhooks/{id}/deliveries:
    parameters:
      - $ref: '#/components/parameters/WorkspaceId'
      - { name: id, in: path, required: true, schema: { type: string } }
    get:
      tags: [webhooks]
      summary: Список последних попыток доставки
      description: "Требует скоуп `webhooks:read`."
      responses: { "200": { description: ok } }

  /workspaces/{workspace_id}/webhooks/deliveries/{delivery_id}/replay:
    parameters:
      - $ref: '#/components/parameters/WorkspaceId'
      - { name: delivery_id, in: path, required: true, schema: { type: string } }
    post:
      tags: [webhooks]
      summary: Повторить прошлую доставку
      description: "Требует скоуп `webhooks:write`."
      x-required-scopes: [webhooks:write]
      responses: { "202": { description: replayed } }

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: API key

  parameters:
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      schema: { type: string, maxLength: 64 }
      description: Уникальный ключ операции, сохраняется 24 часа.
    WorkspaceId:
      name: workspace_id
      in: path
      required: true
      schema: { type: string }
      description: ID рабочего пространства (см. `GET /workspaces`). Должен совпадать с воркспейсом API-ключа.

  responses:
    Unauthorized:
      description: отсутствует или неверный API-ключ
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    Forbidden:
      description: ключ не авторизован для этого ресурса/рабочего пространства
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    Invalid:
      description: ошибка валидации запроса
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }

  schemas:
    Workspace:
      type: object
      properties:
        id: { type: string }
        name: { type: string }
        plan: { type: string }
        data_region: { type: string, enum: [ru, eu, us] }

    Contact:
      type: object
      properties:
        id: { type: string }
        external_id: { type: string, nullable: true }
        email: { type: string, nullable: true }
        phone: { type: string, nullable: true }
        status: { type: string, enum: [active, unsubscribed, bounced, complained] }
        attributes: { type: object, additionalProperties: true }
        tags: { type: array, items: { type: string } }
        locale: { type: string, nullable: true }
        timezone: { type: string, nullable: true }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }

    ContactUpsert:
      type: object
      properties:
        external_id: { type: string }
        email: { type: string, format: email }
        phone: { type: string }
        attributes: { type: object, additionalProperties: true }
        tags: { type: array, items: { type: string } }
        locale: { type: string }
        timezone: { type: string }
        list_ids: { type: array, items: { type: string } }
        consent:
          type: object
          properties:
            marketing_email: { type: boolean }
            sms: { type: boolean }

    ContactPatch:
      allOf:
        - $ref: '#/components/schemas/ContactUpsert'

    ContactPage:
      type: object
      properties:
        data:
          type: array
          items: { $ref: '#/components/schemas/Contact' }
        next_cursor: { type: string, nullable: true }

    Segment:
      type: object
      required: [name, filter]
      properties:
        id: { type: string, readOnly: true }
        name: { type: string }
        kind: { type: string, enum: [live, snapshot], default: live }
        filter:
          description: DSL фильтрации
          type: object
          example:
            and:
              - attribute: country
                op: eq
                value: RU
              - event: purchase
                in_last_days: 30
                count: { op: gte, value: 1 }

    Template:
      type: object
      required: [name, channel]
      properties:
        id: { type: string, readOnly: true }
        name: { type: string }
        channel: { type: string, enum: [email, sms, push, telegram, whatsapp, viber] }
        subject: { type: string }
        preheader: { type: string }
        from_name: { type: string }
        from_email: { type: string, format: email }
        reply_to: { type: string, format: email }
        html: { type: string }
        mjml: { type: string }
        text: { type: string }
        variables: { type: array, items: { type: string } }

    Campaign:
      type: object
      required: [name, channel, template_id, audience]
      properties:
        id: { type: string, readOnly: true }
        name: { type: string }
        channel: { type: string, enum: [email, sms, push, telegram, whatsapp, viber] }
        kind: { type: string, enum: [regular, ab, rss, recurring] }
        template_id: { type: string }
        subject_override: { type: string }
        from_name: { type: string }
        from_email: { type: string }
        reply_to: { type: string }
        audience:
          type: object
          properties:
            list_ids: { type: array, items: { type: string } }
            segment_ids: { type: array, items: { type: string } }
            exclude_list_ids: { type: array, items: { type: string } }
        variants:
          type: array
          items:
            type: object
            properties:
              name: { type: string }
              weight: { type: number }
              subject: { type: string }
              template_id: { type: string }
        settings:
          type: object
          properties:
            utm:
              type: object
              properties:
                source: { type: string }
                medium: { type: string }
                campaign: { type: string }
            quiet_hours:
              type: object
              properties:
                start: { type: string, example: "21:00" }
                end: { type: string, example: "09:00" }
            frequency_cap:
              type: object
              properties:
                per_day: { type: integer }
                per_week: { type: integer }

    CampaignStats:
      type: object
      properties:
        campaign_id: { type: string }
        sent: { type: integer }
        delivered: { type: integer }
        opens: { type: integer }
        unique_opens: { type: integer }
        clicks: { type: integer }
        unique_clicks: { type: integer }
        bounces: { type: integer }
        complaints: { type: integer }
        unsubscribes: { type: integer }
        open_rate: { type: number, format: float }
        click_rate: { type: number, format: float }

    MessageRequest:
      type: object
      required: [channel, to]
      properties:
        channel: { type: string, enum: [email, sms, push, telegram, whatsapp, viber] }
        to:
          type: string
          description: "Получатель: email / телефон в E.164 / chat_id и т.п. — в зависимости от канала."
        cc:
          type: array
          items: { type: string, format: email }
          description: "Копия (email). Видна остальным получателям. До 50 адресов."
        bcc:
          type: array
          items: { type: string, format: email }
          description: "Скрытая копия (email). Не попадает в заголовки письма — получатели друг друга не видят. До 50 адресов."
        contact_id:
          type: string
          description: "Альтернатива to: отправка существующему контакту по его id."
        template_id:
          type: string
          description: "Если задан, subject/html/text берутся из шаблона, а variables подставляются."
        from_email:
          type: string
          format: email
          description: "Адрес отправителя. Должен принадлежать верифицированному отправляющему домену."
        from_name: { type: string }
        reply_to: { type: string, format: email }
        headers:
          type: object
          additionalProperties: { type: string }
          description: >-
            Произвольные MIME-заголовки письма (например `X-Entity-Ref-ID`).
            Служебные и адресные заголовки (From, To, Subject, Date, Message-ID,
            DKIM-Signature, List-Unsubscribe и т.п.) переопределить нельзя. До 30 штук.
        subject: { type: string, description: "Тема письма. Игнорируется, если задан template_id." }
        html: { type: string, description: "HTML-тело (если без шаблона)." }
        text: { type: string, description: "Текстовая часть." }
        variables: { type: object, additionalProperties: true, description: "Подстановки в шаблон." }
        metadata: { type: object, additionalProperties: true, description: "Произвольные метаданные, возвращаются в веб-хуках о доставке." }
        tags: { type: array, items: { type: string } }
        attachments:
          type: array
          description: "Вложения письма. До 10 штук, суммарно до 15 MiB в декодированном виде."
          items:
            type: object
            required: [filename, content_b64]
            properties:
              filename: { type: string, description: "Имя файла. Без переводов строк." }
              content_type: { type: string, description: "MIME-тип. Можно не указывать — определяется автоматически по расширению файла и сигнатуре (pdf, docx, xlsx, png, jpg и др.)." }
              content_b64: { type: string, description: "Содержимое файла в base64." }
        sending_domain_id:
          type: string
          description: "Явный отправляющий домен; по умолчанию выбирается автоматически по from_email."

    MessageStatus:
      type: object
      properties:
        message_id: { type: string }
        status: { type: string, enum: [queued, rendered, handed_off, delivered, deferred, bounced, complained, dropped, opened, clicked] }
        events:
          type: array
          items:
            type: object
            properties:
              kind: { type: string }
              at: { type: string, format: date-time }
              meta: { type: object }

    ProductEvent:
      type: object
      required: [name, contact]
      properties:
        name: { type: string, example: "purchase" }
        contact:
          type: object
          properties:
            id: { type: string }
            external_id: { type: string }
            email: { type: string, format: email }
        occurred_at: { type: string, format: date-time }
        properties: { type: object, additionalProperties: true }

    Automation:
      type: object
      required: [name, trigger, graph]
      properties:
        id: { type: string, readOnly: true }
        name: { type: string }
        status: { type: string, enum: [draft, active, paused] }
        trigger:
          type: object
          properties:
            kind: { type: string, enum: [event, list_joined, attribute_changed, schedule, date_anchor, webhook] }
            config: { type: object }
        graph:
          type: object
          description: ориентированный граф нод; формализация в отдельной спеке
        re_entry: { type: string, enum: [allow, once, cooldown], default: once }

    SendingDomain:
      type: object
      properties:
        id: { type: string }
        domain: { type: string }
        tracking_subdomain: { type: string }
        status: { type: string, enum: [pending, verified, failed] }
        dkim:
          type: array
          items:
            type: object
            properties:
              selector: { type: string }
              record: { type: string }
              status: { type: string, enum: [pending, verified, failed] }
        spf:
          type: object
          properties:
            record: { type: string }
            status: { type: string, enum: [pending, verified, failed] }
        dmarc:
          type: object
          properties:
            record: { type: string }
            status: { type: string, enum: [pending, verified, failed] }

    Webhook:
      type: object
      required: [url, events]
      properties:
        id: { type: string, readOnly: true }
        url: { type: string, format: uri }
        events:
          type: array
          items:
            type: string
            enum:
              - email.delivered
              - email.bounce
              - email.complaint
              - email.unsubscribe
              - email.opened
              - email.clicked
              - sms.delivered
              - sms.failed
              - contact.subscribed
              - contact.unsubscribed
              - contact.changed
              - campaign.finished
        secret: { type: string, readOnly: true, description: "виден только при создании" }
        status: { type: string, enum: [active, paused] }

    Error:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message]
          properties:
            code: { type: string }
            message: { type: string }
            details: { type: object }
            request_id: { type: string }
