# Интеграции и письма

## Состояние интеграций и журнал отправок

Права и тариф для всех методов этого раздела: тарифная опция самой интеграции
плюс право участника **`integrations`**. Право закрывает даже чтение — участник
без него не должен видеть, куда уходят ответы. Владелец воркспейса проходит
всегда.

Отказы:

| Ответ | Когда |
|---|---|
| `403` + `blocked_by_access: true` | у участника нет права `integrations` |
| `403` + `blocked_by_tariff: true` | тариф не включает эту интеграцию |
| `400` + `code: not_connected` | журнал запрошен у неподключённой интеграции |

---

### 47. Состояние интеграций опроса

*Метод в API с 2026-09-08.*

Отвечает на «почему заявки не приходят»: по каждой интеграции сразу видно,
подключена ли она, включена ли отправка и разрешает ли её тариф.

**HTTP:** `GET /quiz/{id}/integrations`  
**Аутентификация:** Обязательна

#### Пример ответа (200)
```json
{
    "status": true,
    "quiz_id": 18547,
    "integrations": {
        "telegram": {"connected": true, "enabled": true, "allowed_by_tariff": true, "blocked": null},
        "max": {"connected": false, "enabled": false, "allowed_by_tariff": true, "blocked": "not_connected"},
        "amocrm": {"connected": true, "enabled": false, "allowed_by_tariff": true, "blocked": "disabled"},
        "bitrix24": {"connected": false, "enabled": false, "allowed_by_tariff": false, "blocked": "blocked_by_tariff"},
        "zapier": {"connected": false, "enabled": false, "allowed_by_tariff": true, "blocked": "not_connected"},
        "google_sheets": {"connected": false, "enabled": false, "allowed_by_tariff": true, "blocked": "not_connected"}
    }
}
```

`blocked` — готовая причина, по которой ответы не уходят: `blocked_by_tariff`,
`not_connected` или `disabled`. Пусто — отправка работает.

Вебхуки в этот ответ не входят: у них своя настройка и свой журнал доставки.

---

### 48. Журнал отправок мессенджера

*Метод в API с 2026-09-08.*

Свой журнал отправок есть у Telegram и MAX. По нему видно причину, по которой
сообщение не ушло: бота выгнали из группы, чат удалён, токен отозван.

**HTTP:** `GET /quiz/{id}/integrations/logs`  
**Аутентификация:** Обязательна

#### Query параметры
| Параметр | Тип | Обязательное | Описание |
|---|---|---|---|
| `integration` | string | Да | `telegram` или `max`. Другие значения — `422`. |
| `page` | integer | Нет | Страница, по умолчанию 1. |
| `per_page` | integer | Нет | Записей на странице, 1–100, по умолчанию 20. |

#### Пример ответа (200)
```json
{
    "status": true,
    "quiz_id": 18547,
    "integration": "telegram",
    "total": 143,
    "page": 1,
    "per_page": 20,
    "last_page": 8,
    "unsent_count": 3,
    "logs": [...]
}
```

`unsent_count` — сколько отправок сорвалось и ждёт повтора.

Неподключённая интеграция отвечает `400` с кодом `not_connected`, а не пустым
списком: журнала в этом случае не существует, и пустой список означал бы, что
отправок не было.

---

### 49. Досылка неотправленного

*Метод в API с 2026-09-08.*

Когда причину отказа устранили — бота вернули в группу, токен перевыпустили, —
накопившееся можно дослать: либо одну запись журнала, либо все неудачные за
период.

**HTTP:** `POST /quiz/{id}/integrations/logs/resend`  
**Аутентификация:** Обязательна

#### Тело запроса (JSON)
| Поле | Тип | Обязательное | Описание |
|---|---|---|---|
| `integration` | string | Да | `telegram` или `max`. |
| `log_id` | integer | Нет | Одна запись журнала. |
| `date_from` | date | Нет | Начало периода для досылки всех неудачных. |
| `date_to` | date | Нет | Конец периода, не раньше `date_from`. |

Без `log_id` и без периода дошлются все неудачные отправки. `log_id` вместе с
периодом — `422`: это разные действия, и вместе они означали бы, что период
молча проигнорирован.

#### Пример ответа (200)
```json
{
    "status": true,
    "quiz_id": 18547,
    "integration": "telegram",
    "resent": 3
}
```

---

## Отправка ответов в CRM

Молчащая интеграция выглядит одинаково при трёх разных причинах: отправка
выключена, тариф её не включает или на стороне CRM отозвали токен. Последнее со
стороны опроса не видно вообще — поэтому проверка связи здесь отдельным методом.

`{crm}` в путях — `amocrm` или `bitrix24`. Другое значение — `422` с кодом
`unknown_crm`. Опрос без подключённой CRM — `400` с кодом `not_connected`:
это не пустое состояние, а отсутствие настройки.

Права и тариф — как во всём разделе: тарифная опция этой CRM плюс право
участника `integrations`.

---

### 50. Состояние подключения CRM

*Метод в API с 2026-09-08.*

**HTTP:** `GET /quiz/{id}/integrations/crm/{crm}`  
**Аутентификация:** Обязательна

#### Пример ответа (200)
```json
{
    "status": true,
    "quiz_id": 18547,
    "crm": "bitrix24",
    "connected": true,
    "is_active": false
}
```

---

### 51. Включить или выключить отправку в CRM

*Метод в API с 2026-09-08.*

**HTTP:** `POST /quiz/{id}/integrations/crm/{crm}/toggle`  
**Аутентификация:** Обязательна

#### Тело запроса (JSON)
| Поле | Тип | Обязательное | Описание |
|---|---|---|---|
| `is_active` | boolean | Да | Включить отправку или выключить. Принимаются и строки `"true"` / `"false"`. |

#### Пример ответа (200)
```json
{"status": true, "quiz_id": 18547, "crm": "bitrix24", "is_active": true}
```

Отказ записи — `400` с кодом `toggle_failed`.

---

### 52. Проверить, живо ли подключение

*Метод в API с 2026-09-08.*

Токен отзывают на стороне CRM, и со стороны опроса это выглядит как молчащая
интеграция. Метод обращается к порталу и отвечает, отвечает ли он.

**HTTP:** `GET /quiz/{id}/integrations/crm/{crm}/check`  
**Аутентификация:** Обязательна

#### Пример ответа (200)
```json
{
    "status": true,
    "quiz_id": 18547,
    "crm": "bitrix24",
    "is_active": true,
    "connection_ok": false
}
```

Мёртвая связь — это результат проверки, а не сбой вызова, поэтому ответ
успешный: смотреть надо на `connection_ok`.

---

### 53. Справочники CRM и текущее сопоставление

*Метод в API с 2026-09-08.*

То же, что видит человек в форме настройки: поля лида, сделки, контакта и
компании, воронки со стадиями, ответственные. Рядом — сопоставление, сохранённое
у опроса сейчас.

**HTTP:** `GET /quiz/{id}/integrations/crm/{crm}/fields`  
**Аутентификация:** Обязательна

#### Query параметры
| Параметр | Тип | Обязательное | Описание |
|---|---|---|---|
| `sections` | array | Нет | `lead`, `deal`, `contact`, `company`, `users`, `pipelines`, `types`. По умолчанию `lead` и `contact`: каждый справочник — отдельный запрос в CRM. Принимается и строкой через запятую. |
| `deal_category_id` | integer | Нет | Воронка, чьи стадии нужны (только Bitrix24). Без неё портал отдаёт стадии воронки по умолчанию. |

#### Пример ответа (200)
```json
{
    "status": true,
    "quiz_id": 18547,
    "crm": "bitrix24",
    "is_active": true,
    "sections": ["lead", "pipelines"],
    "directories": {
        "lead": {...},
        "deal_categories": [...],
        "deal_stages": [...]
    },
    "current_mapping": {"lead": {"enabled": true, "fields": {...}}}
}
```

Различия CRM отдаются как есть, а не подгоняются друг под друга: у amoCRM
сделка и есть лид, поэтому разделы `deal` и `types` приходят со значением
`null` — это ответ, а не сбой. У Bitrix24 раздел `pipelines` возвращает два
справочника: воронки (`deal_categories`) и стадии (`deal_stages`).

Отозванный доступ к порталу — `400` с кодом `connection_lost`, сбой обращения к
справочникам — `400` с кодом `directories_failed`. Справочники читаются живыми
запросами в CRM, поэтому вызов не мгновенный.

---

## Сопоставление полей опроса с полями CRM

Неверная форма сопоставления **не вызывает ошибки при сохранении**, а молча ломает
доставку: заявки начинают приходить пустыми или встают не в ту воронку, и
заметить это можно много позже. Поэтому каждое значение проверяется до записи.

Две CRM устроены по-разному, поэтому и методы разные. За один вызов правится
одна сущность или один блок: правка одного не сбрасывает остальное, а
непереданные ключи остаются прежними.

---

### 121. Bitrix24: сопоставление по сущностям

*Метод в API с 2026-09-08.*

**HTTP:** `POST /quiz/{id}/integrations/bitrix24/mapping/{entity}`

`{entity}` — `lead`, `contact`, `company`, `deal`; другое значение — `422` с
кодом `unknown_entity`.

| Поле | Тип | Описание |
|---|---|---|
| `enabled` | boolean | Создавать эту сущность при ответе. |
| `fields` | object | `{"ПОЛЕ": {"widget_id": "..."}}` или `{"ПОЛЕ": {"static_value": "..."}}`, до 200 полей. |
| `title_type` | string | `Field` — название из вопроса, `Static` — заданный текст. |
| `title_value` | string | Идентификатор вопроса или сам текст, смотря по `title_type`. |
| `company_title_value` | string | Название компании. |
| `status` | string | Стадия лида (только `lead`). |
| `add_id_to_title` | boolean | Добавлять номер к названию (только `lead`). |
| `category_id` | integer | Воронка (только `deal`). |
| `default_stage` | string | Стадия сделки (только `deal`). |
| `link_to_lead` | boolean | Привязать сделку к лиду (только `deal`). |

**Набор полей заменяется целиком**, но только когда его передали: вызов без
`fields` оставляет прежний набор. Каждое поле берёт значение откуда-то одного —
поле с обоими источниками даёт `422` с кодом `field_ambiguous` (адаптер в этом
случае взял бы вопрос и молча проигнорировал постоянное значение), поле без
источников — `field_empty`.

**Стадия принадлежит воронке.** У воронки по умолчанию стадия не начинается с
«C», у воронки N обязана начинаться с «CN:». Несовместимая — `422` с кодом
`stage_mismatch`; стадия у сущности, кроме сделки — `stage_deal_only`. Воронка
берётся из вызова, а если её не передали — из уже сохранённых настроек.

В ответе есть `stage_dropped`: сервис молча выбрасывает стадию, которую не
принял, и это единственный способ узнать, что она не записалась.

---

### 122. amoCRM: сопоставление по блокам

*Метод в API с 2026-09-08.*

**HTTP:** `POST /quiz/{id}/integrations/amocrm/mapping/{block}`

`{block}` — `deal` (сделка), `list` (контакты, компании, каталоги), `task`
(задача); другое значение — `422` с кодом `unknown_block`.

| Поле | Тип | Описание |
|---|---|---|
| `active` | array | Что создавать: номера воронок у сделки, `0` — контакт и `1` — компания у списков, номера каталогов портала. |
| `titles` | object | Название по номеру воронки или списка: идентификатор вопроса либо текст. |
| `statuses` | object | Стадия по номеру воронки. |
| `users` | object | Ответственный по номеру воронки или списка. |
| `use_price` | object | Брать сумму сделки из набранных баллов. |
| `bind` | object | `{"ID поля CRM": "UUID вопроса"}`, до 200 полей. |
| `subtype` | object | Уточнение вида значения у сопоставленного поля. |

**Каждое значение сверяется с живыми справочниками портала.** Отказы приходят
кодом: `pipeline_unknown` (нет такой воронки), `status_mismatch` (стадия из
другой воронки), `field_unknown` (нет такого поля), `bind_not_widget` (значение
поля берётся из ответа, поэтому здесь нужен вопрос, а не текст),
`subtype_without_bind`, `user_unknown`, `title_empty`,
`directories_unavailable`.

Пустой список `active` означает «не создавать» — это значение, а не отсутствие
значения, и оно сохраняется. Ключи, которых нет в этом методе (например значения
полей-списков, их заводит кабинет), сохраняются как были.

Без живого подключения к порталу метод отвечает `400` с кодом `connection_lost`
и **ничего не записывает**: справочников не прочитать, а проверять нечем.

Прежний метод `POST /amocrm/quiz/{id}/settings` остаётся как есть, но он
принимает массив настроек без проверок — на новых интеграциях лучше
использовать этот.

---

## Интеграции

### AmoCRM: Аккаунт

*Метод в API с 2026-02-19.*

Получает общую информацию об аккаунте AmoCRM: воронки, пользователи, типы задач и дополнительные поля.

**HTTP:** `GET /amocrm/account`  
**Аутентификация:** Обязательна

#### Пример ответа (200)
```json
{
    "pipelines": [...],
    "users": [...],
    "task_types": [...],
    "custom_fields": [...]
}
```

---

### AmoCRM: Списки

*Метод в API с 2026-02-19.*

Получает каталоги (списки) из AmoCRM.

**HTTP:** `GET /amocrm/lists`  
**Аутентификация:** Обязательна

#### Пример ответа (200)
```json
{
    "response": {
        "items": [...]
    }
}
```

---

### AmoCRM: Опросы

*Метод в API с 2026-02-19.*

Возвращает список опросов пользователя с информацией об активности интеграции AmoCRM.

**HTTP:** `GET /amocrm/quiz`  
**Аутентификация:** Обязательна

#### Query параметры
Аналогичны `GET /quiz` (`sort`, `order`, `limit`, `offset`, `folder_id`).

#### Пример ответа (200)
```json
[
    {
        "id": 123,
        "name": "Опрос с Amo",
        "amocrm_is_active": true,
        "folder": { ... }
    }
]
```

---

### AmoCRM: Настройки опроса

*Метод в API с 2026-02-19.*

Получение или сохранение настроек AmoCRM для конкретного опроса.

**Получение:** `GET /amocrm/quiz/{id}/settings`  
**Сохранение:** `POST /amocrm/quiz/{id}/settings`

#### Тело запроса для сохранения (JSON)
| Поле | Тип | Обязательное | Описание |
|------|-----|--------------|----------|
| `settings` | array | Да | Массив настроек сопоставления полей |
| `is_active` | boolean | Нет | Флаг активности интеграции |

---

## Вебхуки опроса

Вебхук уводит ответы в свою систему. Значения заголовков в ответах подменяются
на `***`: там носят ключи и токены доступа, отдавать их обратно незачем. Имена
заголовков видны — по ним понятно, что настроено.

Тариф проверяется на записи: настраивать отправку, которой тариф не даст
сработать, смысла нет. Выключить вебхук можно на любом тарифе — запрет касается
настройки, а не отказа от неё.

---

### 54. Список вебхуков

*Метод в API с 2026-09-08.*

**HTTP:** `GET /quiz/{id}/webhooks`

```json
{
    "status": true,
    "quiz_id": 18547,
    "is_active": true,
    "items": [{"id": 50, "title": "Наша CRM", "url": "https://example.com/hook",
               "method": "post", "is_active": true, "incomplete": false, "hours": 0,
               "headers": [{"name": "Authorization", "value": "***"}]}]
}
```

---

### 55. Создать вебхук

*Метод в API с 2026-09-08.*

**HTTP:** `POST /quiz/{id}/webhooks`

| Поле | Тип | Обязательное | Описание |
|---|---|---|---|
| `title` | string | Да | Название, до 255 символов. |
| `url` | string | Да | Адрес. Только публичный http(s): адрес закрытой сети отклоняется. |
| `method` | string | Да | `get`, `post`, `put`, `patch`. |
| `body` | array | Нет | Состав отправляемых данных. |
| `body_naming` | array | Нет | Свои имена полей. |
| `headers` | array | Нет | `[{"name": "...", "value": "..."}]`. |
| `incomplete` | boolean | Нет | Отправлять незавершённые ответы. |
| `hours` | integer | Нет | Через сколько часов считать ответ незавершённым: `0`, `1`, `6`, `12`, `24`. |
| `is_active` | boolean | Нет | Сразу включить отправку. По умолчанию вебхук создаётся выключенным. |

В ответе — `webhook_id` нового вебхука и полный список `items`.

Повторный вызов с тем же `url` и `method` вебхук **не** задваивает: приходит
`400` с кодом `webhook_exists` и `webhook_id` уже существующего. Иначе приёмник
начал бы получать по два запроса на каждый ответ.

---

### 56. Изменить вебхук

*Метод в API с 2026-09-08.*

**HTTP:** `POST /quiz/{id}/webhooks/{webhookId}`

Поля те же, все необязательные: непереданное остаётся прежним. **Заголовки при
этом сохраняются**, хотя запись перезаписывает вебхук целиком — прежнее
состояние подмешивается на нашей стороне. Передали `headers` — они заменяют
прежний список полностью.

Неизвестный вебхук — `400` с кодом `webhook_not_found`.

---

### 57. Удалить вебхук

*Метод в API с 2026-09-08.*

**HTTP:** `DELETE /quiz/{id}/webhooks/{webhookId}`

---

### 58. Включить или выключить вебхук

*Метод в API с 2026-09-08.*

**HTTP:** `POST /quiz/{id}/webhooks/{webhookId}/toggle`

| Поле | Тип | Обязательное |
|---|---|---|
| `is_active` | boolean | Да |

---

### 59. Журнал доставки вебхука

*Метод в API с 2026-09-08.*

**HTTP:** `GET /quiz/{id}/webhooks/{webhookId}/logs`

| Параметр | Тип | Описание |
|---|---|---|
| `page` | integer | Страница, по умолчанию 1. |
| `per_page` | integer | 1–30, по умолчанию 20. В журнале лежат тела запросов и ответов, поэтому предел ниже обычного. |

Значения заголовков в записях журнала подменяются на `***`, как и в списке
вебхуков: журнал хранит те же ключи доступа к приёмнику, которыми пользуется
досылка. Имена заголовков видны.

---

### 60. Досылка вебхука

*Метод в API с 2026-09-08.*

**HTTP:** `POST /quiz/{id}/webhooks/{webhookId}/logs/resend`

| Поле | Тип | Описание |
|---|---|---|
| `log_id` | integer | Одна запись журнала. |
| `date_from` | date | `Y-m-d`. Начало периода. |
| `date_to` | date | `Y-m-d`. Конец периода. |

`log_id` вместе с периодом — `422`.

---

## Уведомления в Telegram и MAX

Тарифная опция мессенджера (`telegram`, `max`) плюс право `integrations`.

---

### 101. Мессенджер опроса

*Метод в API с 2026-09-08.*

| Действие | HTTP |
|---|---|
| Состояние | `GET /quiz/{id}/integrations/messenger/{messenger}` |
| Действия | `POST /quiz/{id}/integrations/messenger/{messenger}` |

`{messenger}` — `telegram` или `max`; другое значение даёт `422` с кодом
`unknown_messenger`.

| Поле | Тип | Описание |
|---|---|---|
| `action` | string | `activate`, `deactivate`, `delete`, `toggle_buttons`, `save_widgets`, `remove_recipient`, `test`. |
| `buttons_enabled` | boolean | Для `toggle_buttons`. Кнопки под сообщением. |
| `subject` | string | Тема сообщения, до 70 символов. |
| `hidden_widget_ids` | array | UUID вопросов, которые не попадают в сообщение, до 500. |
| `recipient_id` | string | Для `remove_recipient`. У Telegram это номер чата, у MAX — идентификатор человека. |

```json
{
    "status": true, "quiz_id": 18547, "messenger": "telegram", "is_active": true,
    "subject": "Новая заявка", "buttons_enabled": true,
    "bot_url": "https://t.me/webask_bot?start=1f0c...",
    "recipients": [{"id": 123456789, "title": "Отдел продаж"}],
    "hidden_widget_ids": ["b3f1c2d4-..."]
}
```

**Подключение делает человек.** `activate` заводит интеграцию и выдаёт `bot_url` —
по этой ссылке бота добавляют в чат или группу; пока этого не сделали, отправлять
некуда. Получатели появляются сами, когда бота добавили.

Список исключённых вопросов **задаётся целиком**: чего в нём нет, то в сообщение
вернётся. Непереданный список не трогается — иначе правка одной темы сняла бы все
исключения, и вопросы с персональными данными снова ушли бы в чат.

`test` отправляет пробное сообщение всем подключённым получателям. Выключенная
интеграция — `400` с кодом `inactive`, интеграция без получателей — `400` с кодом
`no_recipients`, сбой отправки — `400` с кодом `test_failed`.

Опрос без подключённого мессенджера — `400` с кодом `not_connected` для всего,
кроме `activate`.

---

## Google Таблицы

Подключение остаётся в кабинете: там вход в аккаунт Google и выбор файла на
Диске. Тарифная опция `googleSheets` плюс право `integrations`.

---

### 102. Выгрузка ответов в таблицу

*Метод в API с 2026-09-08.*

| Действие | HTTP |
|---|---|
| Состояние | `GET /quiz/{id}/integrations/google-sheets` |
| Действия | `POST /quiz/{id}/integrations/google-sheets` |

| Поле | Тип | Описание |
|---|---|---|
| `action` | string | `activate`, `deactivate`, `delete`, `save_widgets`, `save_switchers`, `set_sync_mode`, `sync_now`, `export_all`. |
| `hidden_widget_ids` | array | Обязателен для `save_widgets`, до 500. Вопросы, которые в таблицу не попадают. |
| `enable_timestamp` | boolean | Столбец со временем ответа. |
| `enable_personal_data` | boolean | Персональные данные респондента. |
| `sync_mode` | string | Для `set_sync_mode`: `realtime`, `scheduled`, `manual`. |
| `schedule_hour`, `schedule_minute` | integer | Обязательны при `scheduled`: 0–23 и 0–59. |
| `export_mode` | string | Для `export_all`: `new_file`, `new_sheet`, `replace`. |

```json
{
    "status": true, "quiz_id": 18547, "is_active": true,
    "table_title": "Ответы опроса", "table_url": "https://docs.google.com/...",
    "access": "ok", "requires_reconnect": false,
    "enable_timestamp": true, "enable_personal_data": false,
    "sync_mode": "scheduled", "schedule_hour": 9, "schedule_minute": 30,
    "last_synced_at": "2026-09-09 09:30:12", "hidden_widget_ids": []
}
```

`access` — состояние доступа к таблице. Чаще всего выгрузка замолкает именно
из-за отозванного на стороне Google доступа, и тогда приходит
`requires_reconnect: true`: переподключение делают в кабинете.

Список исключённых вопросов **задаётся целиком**, поэтому для `save_widgets` он
обязателен: вызов без него снял бы все исключения, и в таблицу поехали бы
вопросы, которые автор оттуда убрал. Для `save_switchers` нужен хотя бы один
переключатель — незаданный не трогается, и вызов без обоих ничего бы не изменил.

`sync_now` досылает всё, что ещё не уехало в таблицу; `export_all` выгружает
накопившиеся ответы заново — новым файлом, новым листом или заменой прежнего.

Опрос без подключённой интеграции — `400` с кодом `not_connected`.

---

## Zapier

Связки собираются на стороне Zapier — здесь только то, слушает ли он этот опрос.
Права и тариф как во всём разделе: опция `zapier` плюс право `integrations`.

---

### 99. Zapier у опроса

*Метод в API с 2026-09-08.*

| Действие | HTTP |
|---|---|
| Состояние | `GET /quiz/{id}/integrations/zapier` |
| Включить, выключить, отключить | `POST /quiz/{id}/integrations/zapier` с полем `action`: `activate`, `deactivate`, `delete` |

```json
{"status": true, "quiz_id": 18547, "connected": true, "is_active": true, "hooks": 2}
```

`hooks` — сколько связок на стороне Zapier слушает опрос.

`delete` сносит и подписки: связка на стороне Zapier перестанет получать ответы,
и собирать её придётся заново там же. В ответе — `removed_hooks`. Опрос без
подключения — `400` с кодом `not_connected`.

---

## Счётчики и пиксели

Google Аналитика, Яндекс.Метрика, пиксель VK и пиксель Meta. У каждого своя
тарифная опция (`googleStats`, `yaMetrika`, `vkPixel`, `fbPixel`) плюс общее для
раздела право `integrations`.

---

### 100. Счётчик опроса

*Метод в API с 2026-09-08.*

| Действие | HTTP |
|---|---|
| Состояние | `GET /quiz/{id}/integrations/counters/{service}` |
| Запись, включение, выключение, отключение | `POST /quiz/{id}/integrations/counters/{service}` |

`{service}` — `google`, `yandex`, `vk`, `fb`. Другое значение — `422` с кодом
`unknown_service`.

| Поле | Тип | Описание |
|---|---|---|
| `action` | string | `save`, `activate`, `deactivate`, `delete`. |
| `counter` | string | Номер счётчика или пикселя, до 20 символов. |
| `version` | string | Только Google: `ga4` или прежняя `ua`. |
| `is_webvisor` | boolean | Только Яндекс: запись действий посетителя. |

```json
{"status": true, "quiz_id": 18547, "service": "yandex", "counter": "87654321", "is_active": true}
```

Номер и признак включённости пишутся **и в запись интеграции, и в версию
опроса** — рендер читает версию. Поэтому включённый счётчик действительно
появляется на странице, а после `delete` копия в версии снимается и счётчик
перестаёт вставляться.

Отключение ненастроенного счётчика — `400` с кодом `not_connected`.

---

## Письма опроса

Копия ответа автору и письмо респонденту. **Код подтверждения адреса наружу не
отдаётся** — это хеш от четырёхзначного числа, то есть девять тысяч вариантов: по
нему чужой адрес подтверждается перебором и начинает получать копии ответов.

---

### 79. Настройки писем опроса

*Метод в API с 2026-09-08.*

**HTTP:** `GET /quiz/{id}/emails`

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

---

### 80. Шаблон письма

*Метод в API с 2026-09-08.*

**HTTP:** `POST /quiz/{id}/emails/template`

Поля с приставкой `owner_` — письмо автору, `client_` — письмо респонденту:
`subject` (до 500), `letter` (до 5000), `use_default_tmpl`, `reply_to_address`,
`send_promo_code`, `formatted_letter` (HTML из редактора, до 50 000),
`raw_html` + `raw_enabled` (полностью свой HTML, работает только со своим SMTP).
Отдельно `client_source` — имя скрытой переменной, из которой берётся адрес
респондента.

---

### 81. Какие вопросы не попадают в письмо

*Метод в API с 2026-09-08.*

**HTTP:** `POST /quiz/{id}/emails/questions`

| Поле | Тип | Описание |
|---|---|---|
| `owner_excluded_questions` | array | UUID вопросов, исключённых из письма автору. |
| `client_excluded_questions` | array | То же для письма респонденту. |

Непереданный список не трогается — у двух писем исключения независимы.
Переданный пустым означает «в письмо идут все вопросы».

UUID сверяется с виджетами **черновика** опроса: выдуманный или чужой
идентификатор даёт `400` с кодом `unknown_questions` и списком в поле `unknown`.
Раньше такой uuid записывался молча и ни на что не влиял.

Настроек писем у опроса ещё нет — `400` с кодом `emails_not_initialized`.

---

### 82. Включить или выключить письмо

*Метод в API с 2026-09-08.*

**HTTP:** `POST /quiz/{id}/emails/toggle`

| Поле | Тип | Обязательное | Описание |
|---|---|---|---|
| `type` | string | Да | `owner` или `client`. |
| `enabled` | boolean | Да | |

Письмо уже в этом состоянии — ответ `200` с `unchanged: true`: внутри сервис
переключает состояние, а не выставляет его, и повторный вызов иначе вернул бы всё
как было.

---

### 83. Получатели копий ответов

*Метод в API с 2026-09-08.*

| Действие | HTTP |
|---|---|
| Добавить | `POST /quiz/{id}/emails/recipients` с полем `email` |
| Удалить | `DELETE /quiz/{id}/emails/recipients/{emailId}` |
| Включить/выключить | `POST /quiz/{id}/emails/recipients/{emailId}/toggle` |
| Подтвердить или переслать код | `POST /quiz/{id}/emails/recipients/{emailId}/confirm` |

Заведённый адрес начинает получать копии только после подтверждения: код уходит
на сам адрес, а не выдаётся вызывающему. Подтверждение — `action: "confirm"` с
полем `confirmation_code`; повторная отправка кода — `action: "resend_code"`.

---

### 84. Своя почтовая служба аккаунта

*Метод в API с 2026-09-08.*

Требует отдельное право участника **`account_smtp`**: в настройках своей почты
лежит пароль от почтового ящика аккаунта.

**HTTP:** `POST /workspace/{workspace_id}/smtp`

| Поле | Тип | Описание |
|---|---|---|
| `action` | string | `show`, `logs`, `enable`, `disable`, `test`. |
| `limit`, `offset` | integer | Для `logs`, до 100 записей. |

Журнал приходит полем `logs`, рядом — `total`, `limit`, `offset`: имя то же, что
у журналов интеграций и вебхуков.

`test` обращается к почтовому серверу и отвечает `200` с полем
`connection_ok`: неудачная проверка — это её результат, а не сбой вызова.

Заведение и правка настроек своей почты остаются в кабинете — API их не
принимает.

---

## Состояние e-mail рассылок

Только чтение. Отправлять и править кампании через API нельзя: письмо уходит
живым людям, и отменить его невозможно — такое решение принимает человек.

---

### 114. Раздел состояния рассылок

*Метод в API с 2026-09-08.*

**HTTP:** `GET /workspace/{workspace_id}/mailings`

| Параметр | Тип | Обязательное | Описание |
|---|---|---|---|
| `section` | string | Да | Что смотрим, список ниже. |
| `campaign_id` | integer | Для `campaign` и `recipients` | |
| `list_id` | integer | Для `contacts` | |
| `search` | string | Нет | Отбор по названию или адресу. |
| `statuses` | array | Нет | Статусы доставки для `recipients`; пустой список — все. |
| `days` | integer | Нет | Окно для `reputation`, 1–365, по умолчанию 30. |
| `limit`, `offset` | integer | Нет | До 200 записей. |

Разделы: `campaigns` — список кампаний, `campaign` — одна кампания,
`recipients` — получатели с результатом доставки, `contact_lists` — списки
контактов со сводкой, `contacts` — контакты одного списка, `templates` —
шаблоны писем, `reputation` — доля отказов и жалоб (по ней почтовые службы
решают, пускать ли письма аккаунта дальше), `balance` — остаток писем на счету,
`settings` — настройки рассылок, `smtp_logs` — журнал отправок.

Разделы читаются по одному: каждый обращается к своему сервису, и тянуть их все
разом означало бы платить за то, чего не спрашивали.

Кампании или списка с таким идентификатором в аккаунте нет — `400` с кодом
`not_found` и полем `section`: по нему видно, чего именно нет. Это отсутствие
записи, а не сбой чтения.

---