> ## Documentation Index
> Fetch the complete documentation index at: https://docs.hubtalk.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Поля кампании

> Повторы, номера, привязка набора, подтверждение согласия, сверка с реестром и идемпотентность.

# Поля кампании

## Настройка повторов

`retry_config` нормализуется при сохранении и возвращается нормализованным.

| Поле          | Тип                       | По умолч. | Правила                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| ------------- | ------------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `attempts`    | integer                   | `1`       | Всего попыток набора на контакт, 1–30. `0` читается как `1`; больше 30 — ограничивается до 30; отрицательное → `422 campaign_spec_invalid`. **При значении по умолчанию ни один номер не перезванивается.**                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `interval_s`  | integer                   | `900`     | Пауза между попытками в секундах. Должна быть `> 0`, когда `attempts > 1`, иначе `422 campaign_spec_invalid`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| `retry_mode`  | `"regular"` \| `"custom"` | `regular` | `custom` использует лестницу `intervals_s` по каждому повтору.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| `intervals_s` | array of integers         | —         | Секунды до каждого повтора в режиме `custom` (подгоняется под `attempts − 1`).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| `rules`       | array                     | —         | Только `expert`. Правило: `{"id"?, "if", "then", "params"?}`. `if` — значение `status_reason` ([Исходы звонка](/ru/v4/web-api/call-status)) либо `short_call` / `goal_not_achieved`; `then` — `wait_for_pass_end` или `stop_calling` (`retry_in` принимается, но не исполняется — предупреждение `rule_action_unsupported`); `params`: `max_matches` (1–30, по умолчанию 1) для любого условия, `max_duration_s` (≥ 10, обязателен) для `short_call`. Идентификаторы присваивает сервер (`r-xxxxxxxx`); присланные вами, если это не отголоски существующих, заменяются. Голый массив правил принимается как устаревшая форма. |

## Номера: `planned_calls`

Каждый объект — одна строка списка контактов, и **его ключи и есть имена колонок**. Понимается всё, что понимает загрузка файла: колонки пакета рынка (`zip`, `state`, `city`), колонки договорных групп (`contract_id`, `role`), `timezone`, `name` и любая переменная, которую читает сценарий агента. Вложенный объект `variables` — синтаксический сахар, он раскладывается в те же колонки (ключ верхнего уровня побеждает такой же ключ внутри `variables`). Значения приводятся к тексту (логические → `true`/`false`, объекты → компактный JSON), поэтому ведущие нули сохраняются.

```json theme={null}
{
  "planned_calls": [
    {"phone": "+77001234567", "name": "Aigerim", "variables": {"debt": "15000"}},
    {"phone": "+77007654321", "name": "Daniyar", "zip": "73301"}
  ]
}
```

Колонка `phone` обязательна в каждой строке; значение меньше чем с десятью цифрами отклоняется как `phone_unreadable` — отклоняется **строка**, а не запрос. Построчные коды замечаний — в [Отчёт по загрузке номеров](/ru/v4/web-api/imports).

## Привязка набора

Внутренних идентификаторов транков и групп у вас нет, поэтому платформа разрешает их сама: **не присылайте `trunk_id`, `route_id` и `from_number` вместе** — и группы, привязанные к вашему агенту, будут найдены по всем транкам, видимым вашей организации.

| Итог              | Результат                                                                               |
| ----------------- | --------------------------------------------------------------------------------------- |
| ровно одна группа | используется она и первый номер её пула                                                 |
| ни одной          | `422 dialing_route_unresolved`                                                          |
| больше одной      | `422 dialing_route_ambiguous`, кандидаты названы в сообщении парами `trunk_id/route_id` |

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

```
GET /v1/dialing-routes?agent_id=collections-agent
```

Ответ — `{"dialing_routes": [{"trunk_id", "trunk_name", "route_id", "route_name", "agents", "caller_numbers"}]}` — без настроек подключения транка и без учётных данных. Пустой `agent_id` перечисляет все маршруты, видимые вашей организации.

<Note>
  **`trunk_id` — якорь.** Назовите его, и остальное следует существующим правилам платформы: группа разрешается из транка и агента при запуске, а номер выбирает сама группа по своей политике исходящего номера. `route_id` или `from_number` **без** `trunk_id` отклоняются кодом `422 dialing_binding_incomplete`: платформа не станет угадывать, какой транк вы имели в виду. Транк, названный явно, но непригодный (агента нет в его группе, пуст пул), при создании не отклоняется — он становится блокером `binding_invalid`.
</Note>

## Подтверждение согласия

Для набора нужно подтверждение согласия: `{"attestation": {"confirmed": true}}`. Запись хранится неизменяемо против той версии данных, которую покрывает, и против формулировки типа согласия кампании, а подписантом выступает ваш API-ключ — выпуск ключа и есть человеческое действие, которому приписывается подпись (платформа записывает, кто и когда выпустил ключ).

Подтверждение одной загрузки **не** переносится на новую; поэтому в `POST …/launch` поле `attestation.import_id` обязательно (→ `422 attestation_import_id_required`), а `import_id`, не являющийся текущей загрузкой кампании, даёт блокер `attestation_stale`. Партии добавляемых звонков подписываются отдельно.

## Сверка с реестром «не звонить»: `dnc_screen`

`dnc_screen` — авторитетный результат. Проверяется каждая читаемая исходная строка до отбраковки по рынку и до дедупликации, поэтому `matched_rows` может быть больше `suppressed_contacts`.

| Поле                      | Тип                             | Что означает                                                                                                                  |
| ------------------------- | ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `status`                  | string                          | `clean` \| `matches` \| `screen_unavailable` (у загрузки нет записанной построчной сверки).                                   |
| `checked_rows`            | integer \| null                 | Читаемые строки, которые проверены.                                                                                           |
| `unscreened_rows`         | integer \| null                 | Строки с нечитаемым телефоном, не проверены.                                                                                  |
| `matched_rows`            | integer \| null                 | Строки, чей номер есть в реестре (до дедупликации).                                                                           |
| `suppressed_contacts`     | integer \| null                 | Уникальные совпавшие номера.                                                                                                  |
| `fingerprint`             | string                          | Детерминированный хеш точного состава совпадений; решение `skip` привязано к нему.                                            |
| `items`                   | array                           | До 200 совпадений: `{"row_number", "phone", "phone_key", "contact_name", "dnc_entry_id", "source", "reason", "recorded_at"}`. |
| `has_more`, `hidden_rows` |                                 | Обрезан ли `items` и насколько.                                                                                               |
| `all_dialable_suppressed` | boolean                         | Все ожидающие контакты совпали.                                                                                               |
| `as_of_launch`            | boolean                         | `false` — живая сверка с реестром; `true` — зафиксированный при запуске снимок.                                               |
| `screened_at`, `audit_id` | number \| null, integer \| null | Когда снят зафиксированный снимок и его аудит-запись.                                                                         |
| `decision`, `import_id`   | string                          | Только у зафиксированного снимка: `clean` \| `skip` и загрузка, которую он покрывает.                                         |

`import.dnc_matches` — совместимая проекция `items` (те же объекты плюс алиас `row` для `row_number`); `import.dnc_matched` равно `matched_rows` (`0`, когда сверка недоступна).

Совпадения останавливают запуск блокером `dnc_matches`. Присланное `dnc: "skip"` записывает то же аудируемое решение, что человек принимает в кабинете. Оно действует для этой конкретной загрузки **и этого `fingerprint`**: новое совпадение или заменённая запись реестра делают решение устаревшим, и `launch-check`/`409` возвращают пересчитанную сверку. Если совпали все ожидающие контакты, `all_contacts_suppressed` остаётся безусловным. `skip` при чистой сверке ничего не записывает, и запуск идёт как обычно.

Значение по умолчанию `dnc: "block"` при явном запуске тоже записывается решением, и более поздний `block` отзывает любой прежний `skip`. Поэтому явный запуск без поля `dnc` никогда не едет на пропуске, который кто-то одобрил в кабинете: присылайте `dnc: "skip"` сами, если запуск означает именно это. Решению нужна загрузка, к которой применяться: `dnc` при запуске кампании без загрузки → `409 dnc_decision_without_upload`; на загрузке без записанной проекции → `409 dnc_screen_required`.

До первого запуска сверка и отчёт только по совпадениям следуют за живым реестром. Операция запуска пересчитывает сверку непосредственно перед переходом и атомарно фиксирует все совпавшие строки вместе с отметкой загрузки, подписью и состоянием кампании. После запуска чтения и отчёты используют этот снимок (`as_of_launch: true`), даже когда реестр меняется. Контакты, совпавшие по ходу кампании, всё равно снимаются в момент набора; `progress` считает их отдельно.

## Идемпотентность запросов по кампаниям

`POST /v1/campaigns` и `POST /v1/campaigns/{id}/calls` работают по одному протоколу:

* Ключ занимается **до** начала работы, атомарно. Второй запрос с тем же ключом, пока первый ещё выполняется, получает `409 idempotency_key_in_flight` — дождитесь первого ответа, а не повторяйте снова.
* Тело фингерпринтится вместе с ключом (канонический JSON, порядок ключей не важен). Тот же ключ с **другим** телом — `422 idempotency_key_reuse`, и никогда не молчаливая переигровка прежнего ответа.
* Тот же ключ с тем же телом переигрывает сохранённый `201` байт в байт.
* Ключи ограничены вашим API-ключом, эндпоинтом и — для `calls` — кампанией, и помнятся сутки.
* При создании отказ **до** появления кампании освобождает ключ; отказ **после** — оставляет занятым. У `calls` любой отказ освобождает ключ.
