> ## 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.

# Управление кампанией

> Запуск, пауза, отмена, прогресс, статистика и добавление партий номеров.

# Управление кампанией

| Метод и путь                          | Что делает                                                                                                                                                                                                                                                                                          |
| ------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `GET /v1/campaigns`                   | список, новые первыми: `limit` (1–200, по умолчанию 50) и `cursor` → `{campaigns, has_more, next_cursor}`. Элементы — краткие представления: `campaign_id, status, name, agent_id, created_via, launched_via, starts_at, ends_at, timezone, timezone_source, accepts_call_additions` — без блокеров |
| `GET /v1/campaigns/{id}`              | объект кампании (`import: null`) с живой/зафиксированной `dnc_screen` и `blockers`/`warnings` пути чтения                                                                                                                                                                                           |
| `GET /v1/campaigns/{id}/launch-check` | `{blockers, warnings, dnc_screen}` — `dnc_screen` здесь всегда **живая**                                                                                                                                                                                                                            |
| `POST /v1/campaigns/{id}/launch`      | `{"attestation": {"confirmed": true, "import_id": "imp-…"}, "dnc": "block"}`. Успех — `200` с объектом кампании (`blockers: []`); отказ — `409 launch_blocked`                                                                                                                                      |
| `POST /v1/campaigns/{id}/calls`       | добавить партию номеров, см. ниже                                                                                                                                                                                                                                                                   |
| `POST /v1/campaigns/{id}/pause`       | `running` → `paused`. Уже набранные звонки доигрывают, новых наборов нет                                                                                                                                                                                                                            |
| `POST /v1/campaigns/{id}/resume`      | `paused` → `running`. Сначала заново прогоняет проверки запуска — за время паузы могло измениться что угодно                                                                                                                                                                                        |
| `POST /v1/campaigns/{id}/cancel`      | любой нефинальный статус → `cancelled`. Необратимо; уже набранные звонки доигрывают                                                                                                                                                                                                                 |
| `GET /v1/campaigns/{id}/progress`     | состояния контактов, удержания, темп, счётчики реестра и частоты                                                                                                                                                                                                                                    |

**Статусы.** `draft` → (запуск) → `scheduled` (когда `starts_at` в будущем) или `running`; `scheduled` становится `running`, когда открывается окно; `running` ⇄ `paused`; `running` → `completed`, когда очередь опустела (ничего не ожидает и ничего не в наборе) — **кроме** кампании с `accepts_call_additions: true`, которая завершается только по наступлении `ends_at`; любой нефинальный статус → `cancelled`. `completed` и `cancelled` финальны. Наступление `ends_at` закрывает любую кампанию.

На `resume` блокеры `no_pending_contacts`, `attestation_required`, `attestation_stale`, `contacts_geo_rejected` и `dnc_screen_required` не применяются.

Ответы `pause` / `resume` / `cancel` содержат объект кампании **без** `blockers` и `warnings`: эти пути не запускают проверки запуска, а пустой массив читался бы как «проверено, всё чисто». Нужны проверки — вызовите `launch-check`.

**Повторить переход — не ошибка.** Кампания, уже находящаяся в целевом состоянии, отвечает `200` со своим текущим объектом: повтор после сетевого таймаута — не второе намерение. Переход, невозможный **из** текущего состояния (возобновить отменённую, поставить на паузу черновик), — `409 campaign_transition_invalid`. Разница между «уже сделано» и «сделать нельзя» — ровно та, которая вам нужна.

**Заблокированный запуск или возобновление — ошибка, а не `200`.** Создание отвечает `201`, потому что кампания и отчёт действительно созданы; `launch` — равно как и `resume`, который прогоняет те же проверки, — ничего не запустивший, отвечает `409 launch_blocked`, и причины едут в том же ответе:

```json theme={null}
{
  "error": {
    "type": "conflict_error",
    "code": "launch_blocked",
    "status": 409,
    "message": "The campaign did not start: error.blockers names every check that did not pass",
    "blockers": [
      {"code": "agent_not_published", "field": "agent_id"},
      {"code": "dnc_matches", "field": "contacts", "dnc_screen": {"status": "matches", "…": "…"}}
    ]
  }
}
```

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

### Добавление номеров в существующую кампанию

```
POST /v1/campaigns/{id}/calls
Idempotency-Key: <уникальная строка>        (обязателен)

{
  "calls": [
    {"phone": "+77001234567", "timezone": "Asia/Almaty", "variables": {"debt": "15000"}},
    {"phone": "+77007654321", "name": "Daniyar"}
  ],
  "attestation": {"confirmed": true}
}
```

Кампании принадлежат агент, привязка набора, правила повторов и рабочие часы; партия несёт только **номера, переменные и подпись**. Каждый элемент устроен как строка `planned_calls` ([Поля кампании](/ru/v4/web-api/campaign-fields)) — его ключи и есть имена колонок. Неизвестные поля верхнего уровня игнорируются: `agent_id` в теле не меняет агента кампании, а ключ `dnc` не обходит реестр.

Партия принимается в любую кампанию, которая не `completed` и не `cancelled`, — то есть в `draft`, `scheduled`, `running` и `paused`, независимо от того, открыта ли она к добавлению звонков. Закрытая отвечает `409 campaign_closed`.

**Порядок отказов**, все до разбора хотя бы одной строки: `422 idempotency_key_required` → `422 calls_required` (пустой `calls`) → `422 calls_batch_too_large` → `422 attestation_invalid` → `404 campaign_not_found` → `409 campaign_closed` → идемпотентность (`422 idempotency_key_reuse` / `409 idempotency_key_in_flight` / переигровка). Затем разбираются строки: `409 market_package_not_installed`, `422 contact_rows_rejected`, `502 contact_file_storage_unavailable` — как при создании, — и два отказа партии ниже.

<Warning>
  **Партия принимается целиком или отклоняется целиком.** Одна строка с ошибкой данных отклоняет всю партию кодом `422 calls_rejected`; один номер из реестра «не звонить» — кодом `422 calls_dnc_matched`, отдельным, чтобы ваша система отличала «исправить строки» от «убрать этих людей из списка». Если в партии проблемы обоих видов, побеждает `calls_rejected`. Структурные отказы разборщика (нет колонки телефона, нет пригодных строк, …) — тоже `calls_rejected`. Для отклонённой партии контакты не создаются и подпись не записывается.
</Warning>

Оба отказа несут `error.import`:

```json theme={null}
{
  "error": {
    "type": "invalid_request_error",
    "code": "calls_rejected",
    "status": 422,
    "message": "…",
    "import": {
      "import_id": "imp-b87d984fe7",
      "status": "refused",
      "refusal_code": "calls_rejected",
      "rows_seen": 2,
      "accepted": 0,
      "warned": 0,
      "rejected": 1,
      "dnc_matched": 0,
      "issues_total": 1,
      "issues": [{"row_number": 2, "code": "phone_unreadable", "severity": "error"}]
    }
  }
}
```

Здесь `issues` несёт первые 50 замечаний; полный отчёт по строкам — `GET /v1/imports/{import_id}/report` (и `?only=dnc_match` для совпадений с реестром). Отклонённая партия сохраняется записью загрузки со `status: "refused"` ровно для этого.

Успех — `201`:

```json theme={null}
{
  "campaign": {
    "campaign_id": "cam-…", "status": "running", "name": "june collections",
    "agent_id": "collections-agent", "created_via": "api", "launched_via": "api",
    "starts_at": null, "ends_at": null, "timezone": "Asia/Almaty",
    "timezone_source": "request", "accepts_call_additions": true
  },
  "import": { "import_id": "imp-…", "status": "accepted", "rows_seen": 2, "accepted": 2, "warned": 0, "rejected": 0, "duplicates_found": 0, "dnc_matched": 0, "refusal_code": "", "issues": {}, "dnc_screen": {"status": "clean", "as_of_launch": false, "…": "…"}, "dnc_matches": [] },
  "blockers": [{"code": "campaign_paused", "field": "status"}],
  "warnings": [{"code": "starts_immediately", "field": ""}, {"code": "frequency_policy_active", "field": ""}]
}
```

`campaign` — краткое представление (тот же объект, что элемент списка). `blockers` означает «принято, но сейчас не набирается, и вот почему»: те же проверки запуска, что у `launch-check`, минус те, которые свежая партия делает неактуальными (`no_pending_contacts`, `attestation_required`, `dnc_screen_required`, `all_contacts_suppressed`, `dnc_matches`), плюс `campaign_paused`. Элементы на этом пути несут только `code` и `field`.

Правила, которые стоит знать, прежде чем гнать номера из CRM:

* **Предел** — до 1000 строк за запрос по умолчанию (настройка развёртывания): больше — `422 calls_batch_too_large`, пустая партия — `422 calls_required`. Один запрос считается один раз в лимите ключа.
* **Подпись** — `attestation` должна быть ровно `{"confirmed": true}`: без других ключей и без `import_id` (иначе `422 attestation_invalid`). Каждая принятая партия подписывается отдельно и привязана к своей загрузке; подпись запуска не переиспользуется для строк, которых на момент запуска не существовало.
* **Часовой пояс** — в кампании с `use_contact_timezone: true` под казахстанским рыночным профилем строка без `timezone` — это ошибка данных (`timezone_required`), и она отклоняет партию: платформа не подставит зону кампании строке, на которую никто не смотрел. У загрузки файла и у `planned_calls` при создании подстановка сохраняется.
* **Дубли** — внутри партии `duplicate_action` работает так же, как для файла. Номер, у которого в кампании уже есть контакт — даже уже набранный, — **принимается как новый контакт и набирается снова**: для адаптера, который сам управляет перезвонами, повтор *и есть* перезвон. Если перезвонами управляет ваша система, оставьте `retry_config.attempts = 1`; при большем бюджете перезвоны станут двухуровневыми.
* **Партия никогда не становится запусковой загрузкой кампании** — зафиксированный снимок реестра, подпись запуска и `dnc_screen` в `GET /v1/campaigns/{id}` продолжают указывать на загрузку, с которой кампанию запускали. Своя сверка партии — в теле `201` и в `GET /v1/imports/{import_id}`.
* **Идемпотентность** — ключ ограничен рамками кампании, поэтому один и тот же ключ с одним и тем же телом, отправленный в две кампании, — это две партии. Отклонённая партия **освобождает** ключ: исправьте строки и пришлите заново с тем же ключом (создастся вторая отклонённая запись загрузки — диагностически полезно, звонков это не касается). Повтор принятой партии переигрывает ответ; тот же ключ с другим телом — `422 idempotency_key_reuse`.

### Кампании, открытые к добавлению номеров

Обычная кампания завершает себя сама, едва очередь опустела, — а завершённая кампания звонков не принимает. Чтобы кампания ждала номера, создайте её с `"accepts_call_additions": true`. Такая кампания:

* **требует `ends_at`** (`422 campaign_open_ends_at_required`) не дальше горизонта оператора — по умолчанию 30 суток (`422 campaign_open_ends_at_too_far`);
* может быть создана вообще без `planned_calls` и стартует пустой (блокера `no_pending_contacts` нет);
* закрывается только наступлением `ends_at` или отменой, но не опустевшей очередью.

Флаг задаётся при создании и потом не меняется; он возвращается в объекте кампании и в списке.

### Прогресс и состояния контактов

`GET /v1/campaigns/{id}/progress`:

```json theme={null}
{
  "status": "running",
  "total": 120,
  "by_state": {"pending": 80, "in_progress": 3, "done": 30, "exhausted": 5, "suppressed": 2},
  "ready": 78,
  "holds": 2,
  "held_reasons": {"quiet_hours": 2},
  "suppressed_reasons": {"dnc_suppressed": 2},
  "dnc_suppressed_at_launch": 1,
  "dnc_suppressed_mid_campaign": 1,
  "backpressure": 0,
  "infra_retries": 0,
  "next_attempt_at": 1788942000.0,
  "frequency": {"held": 0, "blocked": 0, "by_governing_cap": {}},
  "rate": {"target_per_minute": 10, "actual_per_minute": 4}
}
```

`by_state` отдаёт собственные названия состояний платформы — без слоя перевода, потому что разница между «дозвонились» и «попытки кончились» важна именно вам. **Состояния с нулевым количеством в ответ не попадают.**

| Состояние     | Что означает                                                  |
| ------------- | ------------------------------------------------------------- |
| `pending`     | ждёт набора                                                   |
| `in_progress` | набирается прямо сейчас                                       |
| `awaiting`    | набран, ждёт вердикта анализа разговора (стратегия `expert`)  |
| `done`        | дозвонились — новых попыток не будет                          |
| `exhausted`   | все попытки израсходованы, дозвониться не удалось             |
| `suppressed`  | исключён до набора (реестр «не звонить», география, политика) |

`ready` — сколько ожидающих контактов набираемы прямо сейчас; `holds` — сколько ожидающих удерживается (причины в `held_reasons`); `suppressed_reasons` раскладывает состояние `suppressed`; `dnc_suppressed_at_launch` + `dnc_suppressed_mid_campaign` = `suppressed_reasons.dnc_suppressed`; `frequency` сообщает, сколько контактов сейчас удержано или заблокировано ограничениями частоты на номер и какие лимиты этим управляют. Ключи причин внутри `held_reasons`, `suppressed_reasons` и `by_governing_cap` — машинные коды, их набор может расти.

<Note>
  Обычная кампания закрывается сама, когда очередь опустела, — кампания, созданная с `accepts_call_additions: true`, не закрывается (её закрывают только `ends_at` или отмена). Отдельного вебхука «кампания завершилась» нет в любом случае — опрашивайте `progress` или `GET /v1/campaigns/{id}`.
</Note>

### Снятие контакта с обзвона

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

| Метод и путь                                | Тело                           | Область                                                                          |
| ------------------------------------------- | ------------------------------ | -------------------------------------------------------------------------------- |
| `POST /v1/campaigns/{id}/contacts/withdraw` | `phones` **или** `contact_ids` | эта кампания в любом статусе, кроме `completed` и `cancelled` (черновик — можно) |
| `POST /v1/contacts/withdraw`                | `phones`                       | все **живые** кампании организации — `scheduled`, `running`, `paused`            |

```bash theme={null}
curl -s -X POST "$BASE_URL/v1/contacts/withdraw" \
  -H "Authorization: Bearer $CFLOW_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"phones": ["+7 701 123 45 67", "77015550011"]}'
```

`200 OK` раскладывает контакты по исходу; все пять ключей присутствуют всегда:

```json theme={null}
{
  "operation_id": "wd-3f9c2a71b0",
  "withdrawn":         [{"contact_id": "cc-…", "campaign_id": "cam-…", "phone": "7011234567"}],
  "already_withdrawn": [],
  "not_withdrawable":  [{"contact_id": "cc-…", "campaign_id": "cam-…", "phone": "7015550011", "reason": "in_call"}],
  "not_found": [],
  "invalid":   []
}
```

* **Что снимается.** Контакт, ожидающий набора (`pending`, включая запланированный перезвон) или ожидающий вердикта аналитики (`awaiting`). Он переходит в `suppressed` с причиной `withdrawn_by_client` и в этой кампании больше не набирается. У контакта в `awaiting` звонок **уже состоялся** — снятие отменяет перезвон, который мог бы назначить вердикт.
* **Что не снимается.** `not_withdrawable` называет причину: `in_call` — звонок идёт или контакт только что взят в набор (снятие трубку не кладёт: используйте `POST /v1/calls/{call_id}/end`, а после звонка снимите контакт ещё раз, если перезвон не нужен), `done`, `exhausted` или `already_suppressed` — исключён раньше самой платформой (реестр «не звонить», частотный лимит, география); прежняя причина сохраняется.
* **Повторять безопасно.** `Idempotency-Key` не нужен: уже снятый контакт вернётся в `already_withdrawn`.
* **Как адресовать.** Номер сопоставляется по **последним десяти цифрам**, поэтому написание значения не имеет, а само значение можно слать строкой или числом. Один номер достаёт все контакты, которые его несут, — в кампании с дублями все сразу. Идентификатор контакта — это `metadata.contact_id` из вебхуков звонка; контакт, который ещё ни разу не набирали, адресуется номером.
* **Ручка по организации достаёт и кабинетные кампании**: все живые кампании организации, как бы они ни были созданы. Черновики и закрытые кампании не трогаются.
* **Частичная обработка.** Один плохой адрес не роняет запрос: в `invalid` вернётся то, что не удалось прочитать (не номер, нет цифр, пустой идентификатор), в `not_found` — то, под что не нашлось контакта, и то и другое ровно в том виде, в каком вы прислали. По умолчанию не больше 1000 адресов на запрос.
* **Последствия.** Если снять последние ожидающие контакты идущей кампании, она завершится на следующем проходе диалера — кроме кампании, открытой к добавлению номеров. Снятый номер вернётся в набор только с новой партией звонков.

<Note>
  Экрана снятия в кабинете пока нет: причина `withdrawn_by_client` видна в разбивке прогресса кампании, а сама операция доступна только через API.
</Note>
