Добавление лидов
POST /v1/campaigns/{id}/leads принимает ваших лидов и готовый первый текст для каждого. Ответ 202 показывает прием пакета, а не отправку сообщений.
#Подготовьте кампанию
#Настройте кампанию в кабинете
Создайте линейную кампанию. Выберите получение первого сообщения из API и откройте прием API-лидов. Первый шаг должен быть готов, но его текст пуст: без AI-персонализации, ротаций слов и вложений. Ручное назначение аккаунтов для этой кампании выключено. Настройте аккаунты, расписание и следующие шаги в кабинете.
#Запустите кампанию
Кампания должна быть активной, без текущей пересборки. Пустая API-кампания ждет ваши пакеты. Проверяйте first_message_mode=api, accepts_api_leads=true и status=active через чтение кампании. Одной настройки 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 КиБ на объект. Лишние поля запрещены на любом уровне запроса.
Архив переписки отдает до 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 показывают ограниченный цикл явно.
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":"Анна"}}]}'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"])
breakconst 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 и результат каждого лида
Ответ содержит объект команды. У каждого входного лида есть элемент 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 422invalid_requestдля всего запроса.daily_lead_quota_exceeded,waiting_lead_quota_exceeded: квота закончилась; остальные элементы могут быть приняты.
Одинаковые элементы внутри пакета обрабатываются последовательно по тем же правилам. Уникальность external_id ограничена кампанией, а не всеми кампаниями кабинета. Для исправленных отклоненных элементов отправьте новый пакет с новым ключом; неизменный повтор сохраняет прежний итог.
#Очередь и первое сообщение
queue_state показывает обработку элемента:
waiting: ждет постановки;queued: задача поставлена;held: постановка удержана, смотритеreasonи состояние лида;failed: постановка не удалась;stopped: лид остановлен до постановки;not_required: постановка не требуется дляexistingилиrejected.
Опрашивайте команду до конечного состояния, затем проверяйте лидов и события. 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 означает, что запись для кабинета еще не включена. Полный список в ошибках, бюджеты в лимитах.