# Добавление лидов

POST /v1/campaigns/{id}/leads принимает ваших лидов и готовый первый текст для каждого. Ответ 202 показывает прием пакета, а не отправку сообщений.

> **Важно.** Доступ к записи включается для кабинета по запросу.

## Подготовьте кампанию

### Настройте кампанию в кабинете

Создайте линейную кампанию. Выберите получение первого сообщения из API и откройте прием API-лидов. Первый шаг должен быть готов, но его текст пуст: без AI-персонализации, ротаций слов и вложений. Ручное назначение аккаунтов для этой кампании выключено. Настройте аккаунты, расписание и следующие шаги в кабинете.

### Запустите кампанию

Кампания должна быть активной, без текущей пересборки. Пустая API-кампания ждет ваши пакеты. Проверяйте `first_message_mode=api`, `accepts_api_leads=true` и `status=active` через [чтение кампании](https://docs.melioralab.io/reference/campaigns/). Одной настройки `accepts_api_leads` для приема недостаточно.

При чтении `first_message_mode` может быть `unknown`, а `accepts_api_leads` может быть `null`, если значение не установлено. Это не подтверждение готовности кампании к приему.

### Выдайте ключу право

Нужно `leads:enroll`, а пространство и кампания должны входить в охват ключа. Для проверки отправок отдельно выдайте `leads:read`, `events:read` и при необходимости `messages:read`.

## Тело запроса

Передавайте `Content-Type: application/json` и объект с единственным полем `leads`: от 1 до 100 элементов. Все тело до 512 КиБ; даже пакет из 100 лидов может превысить этот размер.

- `username` обязателен: ник Telegram, `@ник` или публичная ссылка на профиль. Регистр и начальный `@` убираются. Нормализованный ник содержит от 4 до 32 латинских букв, цифр и `_`, начинается с буквы, заканчивается буквой или цифрой. Телефон и приглашение в чат не подходят.
- `external_id` необязателен, допускает `null`: от 1 до 128 символов без управляющих знаков. Это ваш ключ участия в конкретной кампании; он отличается от `fields.record_id`, который связывает карточку с CRM.
- `first_message` обязателен: от 1 до 4096 символов Unicode после замены CRLF/CR на LF, не только пробелы. Пустая строка и нулевой символ запрещены. Пробелы по краям сохраняются.
- `fields` необязателен: только `record_id`, `first_name`, `last_name`, `company`, `position`, `website`, `source`, `utm_source`, `utm_medium`, `utm_campaign`. Значения строковые или `null`; `record_id` до 128 символов, остальные до 1024. До 50 ключей и 8 КиБ на объект. Лишние поля запрещены на любом уровне запроса.

> **Внимание.** Первое сообщение отправляется буквально: без подстановки `{{name}}`, AI, ротаций и разбора Markdown/HTML. Подготовьте окончательный текст у себя. От 700 символов элемент получает предупреждение `first_message_long`, но текст до 4096 символов остается допустимым. Изменение шаблона кампании не заменяет уже принятый текст.

Архив переписки отдает до 4000 символов с признаком сокращения. Допустимый первый текст длиной 4096 символов не обязательно вернется из архива целиком.

## Идемпотентность и повторы

Обязательный `Idempotency-Key` содержит от 1 до 255 печатных ASCII-символов. Создайте уникальное значение для одной логической операции и сохраните его вместе с целью и исходным телом до запроса.

- При 429, 503, таймауте или обрыве повторяйте исходный запрос с тем же ключом. После 429/503 выдержите `Retry-After` и случайный разброс. Не создавайте новый ключ внутри цикла повтора.
- Повтор принятого запроса возвращает ту же команду с ее текущим состоянием, без новых лидов и повторного расхода суточной квоты. HTTP-бюджет расходуется и на повторы.
- Ключ действует в пределах кабинета и сохраняет смысл при перевыпуске API-ключа.
- Другие тело, порядок лидов, цель или операция с тем же ключом дают 422 `idempotency_key_reused`. Даже если два текста становятся одинаковыми после нормализации переносов строк, сохраняйте исходное тело при повторе.

Примеры берут заранее сохраненный `MELIORA_IDEMPOTENCY_KEY`. Пример curl повторяет 429 и временные ошибки с учетом `Retry-After`; Python и Node.js показывают ограниченный цикл явно.

```bash
curl --retry 5 --retry-max-time 300 \
  -X POST https://api.melioralab.io/v1/campaigns/4d8b2f73-6e1a-4c95-b7d0-8a3f5e2c9b14/leads \
  -H "Authorization: Bearer $MELIORA_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $MELIORA_IDEMPOTENCY_KEY" \
  --data-raw '{"leads":[{"username":"example_lead","external_id":"crm-1042","first_message":"Здравствуйте, Анна! Можно задать вопрос?","fields":{"record_id":"crm-1042","first_name":"Анна"}}]}'
```
```python
import json
import os
import random
import time
import requests

url = "https://api.melioralab.io/v1/campaigns/4d8b2f73-6e1a-4c95-b7d0-8a3f5e2c9b14/leads"
headers = {
    "Authorization": f"Bearer {os.environ['MELIORA_API_KEY']}",
    "Content-Type": "application/json",
    "Idempotency-Key": os.environ["MELIORA_IDEMPOTENCY_KEY"],
}
payload = json.dumps({"leads": [{
    "username": "example_lead", "external_id": "crm-1042",
    "first_message": "Здравствуйте, Анна! Можно задать вопрос?",
    "fields": {"record_id": "crm-1042", "first_name": "Анна"},
}]}, ensure_ascii=False).encode("utf-8")

for attempt in range(6):
    response = requests.post(url, headers=headers, data=payload, timeout=15)
    if response.status_code in (429, 503) and attempt < 5:
        delay = max(float(response.headers.get("Retry-After", "5")), min(60, 2 ** attempt))
        time.sleep(delay + random.uniform(0, 1))
        continue
    response.raise_for_status()
    command = response.json()
    print(command["id"], command["items"])
    break
```
```js
const url = 'https://api.melioralab.io/v1/campaigns/4d8b2f73-6e1a-4c95-b7d0-8a3f5e2c9b14/leads';
const headers = {
  Authorization: `Bearer ${process.env.MELIORA_API_KEY}`,
  'Content-Type': 'application/json',
  'Idempotency-Key': process.env.MELIORA_IDEMPOTENCY_KEY,
};
const payload = JSON.stringify({ leads: [{
  username: 'example_lead', external_id: 'crm-1042',
  first_message: 'Здравствуйте, Анна! Можно задать вопрос?',
  fields: { record_id: 'crm-1042', first_name: 'Анна' },
}] });
for (let attempt = 0; attempt < 6; attempt++) {
  const response = await fetch(url, {
    method: 'POST', headers, body: payload, signal: AbortSignal.timeout(15_000),
  });
  if ([429, 503].includes(response.status) && attempt < 5) {
    const delay = Math.max(Number(response.headers.get('Retry-After') ?? 5), Math.min(60, 2 ** attempt));
    await response.body?.cancel();
    await new Promise((resolve) => setTimeout(resolve, (delay + Math.random()) * 1000));
    continue;
  }
  if (!response.ok) throw new Error(`Meliora API ${response.status}: ${await response.text()}`);
  const command = await response.json();
  console.log(command.id, command.items);
  break;
}
```

Если запрос оборвался, сохраненные ключ и тело позволяют повторить его тем же способом после паузы.

## Ответ 202 и результат каждого лида

Ответ содержит объект [команды](https://docs.melioralab.io/reference/commands/). У каждого входного лида есть элемент `items` в исходном порядке с `input_index` от нуля, `lead_id`, нормализованным `username`, `external_id`, `status`, `reason`, `warnings` и `queue_state`. Текст первого сообщения в ответ не возвращается.

- `accepted`: новый лид принят к постановке. Начальный `queue_state=waiting`.
- `existing`: в этой кампании уже есть тот же ненулевой `external_id` и тот же нормализованный ник. Текст и поля не перезаписываются, повторная отправка не создается; `queue_state=not_required`.
- `rejected`: лид не принят, причина в `reason`; обычно `lead_id=null`, `queue_state=not_required`.

`result.accepted` считает новых лидов, принятых к постановке. `result.inserted` считает созданные записи и может включать запись, отклоненную политикой после создания. `existing` не входит в эти счетчики. Если новых принятых лидов нет, команда может сразу иметь `state=done`.

Структурно неверное тело дает ошибку всего запроса. При корректном теле возможен частичный прием: всегда проверяйте каждый `items`, даже при HTTP 202.

## Конфликты и отказы

- `external_id_conflict`: этот `external_id` уже связан с другим ником в кампании.
- `username_already_in_campaign`: ник уже есть в кампании, но пара `external_id` и ника не совпала. Это касается и повторного ника без `external_id`.
- `stoplisted`: контакт в стоп-листе.
- `previous_contact`: раньше уже контактировали, а кампания это запрещает.
- `policy_blocked`: карточка создана, но политика контакта запретила постановку.
- `invalid_username`, `invalid_external_id`, `invalid_first_message`, `invalid_fields`: неверное значение элемента. Часть таких ошибок выявляется раньше и возвращается как HTTP 422 `invalid_request` для всего запроса.
- `daily_lead_quota_exceeded`, `waiting_lead_quota_exceeded`: квота закончилась; остальные элементы могут быть приняты.

Одинаковые элементы внутри пакета обрабатываются последовательно по тем же правилам. Уникальность `external_id` ограничена кампанией, а не всеми кампаниями кабинета. Для исправленных отклоненных элементов отправьте новый пакет с новым ключом; неизменный повтор сохраняет прежний итог.

## Очередь и первое сообщение

`queue_state` показывает обработку элемента:

- `waiting`: ждет постановки;
- `queued`: задача поставлена;
- `held`: постановка удержана, смотрите `reason` и состояние лида;
- `failed`: постановка не удалась;
- `stopped`: лид остановлен до постановки;
- `not_required`: постановка не требуется для `existing` или `rejected`.

Опрашивайте [команду](https://docs.melioralab.io/commands/) до конечного состояния, затем проверяйте лидов и события. `queued` и `done` не подтверждают отправку. Первое сообщение уходит, когда наступает расписание и доступны аккаунт, лимиты и действующая подписка. Точное время не обещается. Подтверждение: `lead.first_message_sent`, `message.sent` или `first_message_sent_at` лида. Шаги 2 и далее идут по обычной цепочке кампании. Неизвестный исход не дает основания повторно отправлять текст.

## Квоты и ошибки запроса

Вес запроса 5. На кабинет допускается до 20 активных команд добавления (`accepted` или `running`). На пространство: до 5000 новых записей за сутки UTC и до 20000 ожидающих работ. В ожидание входят уже ожидающие и выполняемые задачи плюс принятые, еще не поставленные элементы. Повторы и `existing` суточную квоту не расходуют.

Квоты могут возвращаться как HTTP 429 или как `items[].reason` при 202. Ошибка `too_many_active_commands` требует дождаться завершения предыдущих команд; суточная квота восстанавливается в следующие сутки UTC, квота ожидания после обработки работ. Заголовок `Retry-After` задает паузу, но не обещает восстановление квоты за это время.

422 `campaign_first_message_not_api` требует изменить режим кампании в кабинете; `campaign_not_accepting` требует проверить готовность и активность кампании. 413 `payload_too_large` требует уменьшить пакет. 403 `api_write_disabled` означает, что запись для кабинета еще не включена. Полный список в [ошибках](https://docs.melioralab.io/errors/), бюджеты в [лимитах](https://docs.melioralab.io/rate-limits/).
