API EN
Ранний доступ. Ключи выдаются по запросу. Как получить ключ

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

#Вебхуки или лента

Вебхуки и лента событий передают одни и те же факты: lead.first_message_sent и lead.replied. Разница в том, кто начинает обмен.

ВебхукиЛента GET /v1/events
Как приходят событияMeliora отправляет POST на ваш адресВаша система опрашивает API
Что нужно с вашей стороныПубличный адрес httpsКлюч API с правом events:read
Где настраиваетсяВ кабинетеВ коде интеграции
Если получатель недоступенПовторы около 3 суток, затем событие нужно повторить вручнуюСобытия ждут в ленте 30 дней
Текст первого сообщенияНе передаетсяinclude_text=true
Проверка record_idНет, поле приходит как загруженоrecord_id_status

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

Их можно использовать вместе: вебхук как быстрый сигнал, лента как страховка. У одного факта одинаковый id в вебхуке и в ленте, поэтому дедупликация по id не даст применить событие дважды.

#Как это работает

  1. В кабинете случается факт: лиду ушло первое сообщение или пришел засчитанный ответ.
  2. Meliora собирает событие и отправляет POST с JSON на каждый подходящий адрес.
  3. Ваша система проверяет подпись, сохраняет событие и отвечает кодом 2xx за 10 секунд.
  4. Если ответа 2xx нет, Meliora повторяет отправку по расписанию с тем же id.

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

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

Вебхуками управляет владелец или администратор кабинета с доступом ко всем пространствам и кампаниям. Остальные участники видят раздел только для просмотра. Ключ API для вебхуков не нужен.

  1. Откройте Настройки › Интеграции, блок Вебхук событий, и нажмите «Добавить адрес».
  2. В поле «Адрес получателя» вставьте адрес вашей системы. «Описание (видно только в кабинете)» помогает отличать адреса.
  3. В блоке «События» отметьте «Отправлено первое сообщение» (lead.first_message_sent), «Получен ответ» (lead.replied) или оба.
  4. В блоке «Кампании» оставьте «Все кампании», чтобы получать события и новых кампаний, или выберите «Выбранные». Если в кабинете несколько пространств, так же выберите пространства.
  5. Если нужен текст ответа лида, включите «Передавать текст ответа».
  6. Если получатель проверяет ключ в заголовке запроса, добавьте его в «Дополнительные заголовки».
  7. Нажмите «Добавить» и скопируйте ключ подписи из блока «Адрес добавлен. Ключ подписи».
  8. Нажмите «Проверить» и откройте «Журнал»: там появится тестовое событие и ответ вашего адреса.

Ключ подписи можно посмотреть снова в любой момент кнопкой «Ключ подписи» на карточке адреса. В кабинете может быть до 10 адресов. В демонстрационном кабинете вебхуки недоступны.

#Требования к адресу

  • Только https:// и порт 443. Адрес с другим портом не сохранится.
  • Имя сервера, а не IP-адрес.
  • Без логина и пароля в адресе. Для доступа по ключу используйте «Дополнительные заголовки».
  • Действующий сертификат TLS для этого имени от публичного центра сертификации. Самоподписанный сертификат не подойдет.
  • Имя должно указывать только на публичные адреса в интернете. Если хотя бы один адрес ведет во внутреннюю сеть, отправка запрещена.
  • У имени должна быть запись A (IPv4): доставка только по IPv6 сейчас не гарантируется.
  • Переадресации не выполняются: ответ 3xx считается ошибкой. Укажите окончательный адрес.
  • Параметры после ? сохраняются и передаются как есть. Длина адреса до 2048 знаков.

Запросы приходят с серверов Meliora. Их адреса не публикуются и могут меняться, поэтому не фильтруйте запросы по IP: проверяйте подпись.

Адрес может содержать секретный путь, поэтому после сохранения кабинет показывает только его начало и последние знаки. Чтобы сменить адрес, нажмите «Изменить» и вставьте новый. Пустое поле оставляет прежний адрес.

#Дополнительные заголовки

Если ваш сервис принимает запросы только с ключом в заголовке, добавьте до 5 заголовков. Имя начинается с x-, например x-api-key, или равно Authorization. Значение до 1024 знаков. После сохранения значения не показываются, их можно только заменить кнопкой «Заменить заголовки». Заголовки подписи и служебные заголовки запроса переопределить нельзя.

#Запрос

Каждое событие приходит отдельным запросом POST с телом JSON в UTF-8. Тело компактное, без отступов, символы кириллицы не экранируются. Размер тела до 256 КиБ.

HTTP
POST /meliora/webhook HTTP/1.1
Host: crm.example.com
Content-Type: application/json
User-Agent: Meliora-Webhooks/1
webhook-id: evt_aab3db461d0542ff88c3124491aa2cc4
webhook-timestamp: 1793092509
webhook-signature: v1,kt8BoIJwYXnW8z5nUWwQitssRUMxKam8Dv+TgAhDV0s=

{"id":"evt_aab3db461d0542ff88c3124491aa2cc4","type":"lead.replied",...}
ЗаголовокЗначение
webhook-idИдентификатор события evt_ и 32 шестнадцатеричных знака. Одинаковый во всех повторах, совпадает с полем id в теле.
webhook-timestampВремя отправки этой попытки, секунды Unix. У каждого повтора свое.
webhook-signatureПодпись v1,<base64>. Во время смены ключа две подписи через пробел.
Content-Typeapplication/json
User-AgentMeliora-Webhooks/1

Кроме них приходят ваши «Дополнительные заголовки», если они заданы. Имена заголовков в HTTP не зависят от регистра: читайте их без учета регистра.

#Тело

ПолеОписание
idИдентификатор события, ключ дедупликации. Тот же, что у события в ленте.
typelead.first_message_sent, lead.replied или webhook.test у тестового события.
api_versionВерсия формата события, сейчас 2026-10-18. Подробнее в руководстве Версии API.
timestampКогда произошел факт. Это не время отправки: повтор приходит с тем же timestamp.
testtrue только у тестового события.
dataДанные события.

Общие поля data описаны в справочнике событий: пространство, кампания, лид, аккаунт отправителя, время первого сообщения и ответ. Ниже примеры и отличия вебхука от ленты.

{
  "id": "evt_61017785512246cdb0f3fafecba197a2",
  "type": "lead.first_message_sent",
  "api_version": "2026-10-18",
  "timestamp": "2026-10-27T09:02:11.204518+00:00",
  "test": false,
  "data": {
    "event_id": "evt_61017785512246cdb0f3fafecba197a2",
    "organization_id": "594b5bf4-aef7-480d-86b4-393390132326",
    "workspace": { "id": "3512de95-fcaa-4f57-b692-5caee3217b3e", "name": "Продажи" },
    "campaign": { "id": "5cad4b4d-356f-4043-ba37-a42f51fc8a87", "name": "Осенняя кампания" },
    "lead": {
      "id": "083f7a84-6cb7-49e1-9b1e-3a6ee783f70e",
      "username": "ivan_p",
      "username_at": "@ivan_p",
      "telegram_url": "https://t.me/ivan_p",
      "username_variants": [
        "ivan_p", "@ivan_p", "t.me/ivan_p", "https://t.me/ivan_p", "http://t.me/ivan_p",
        "https://t.me/ivan_p/", "telegram.me/ivan_p", "https://telegram.me/ivan_p"
      ],
      "telegram_user_id": 5012345678,
      "status": "contacted",
      "fields": { "record_id": "crm-10482", "first_name": "Иван", "company": "ООО Пример" }
    },
    "account": { "id": "aaa3f0d2-63a1-44df-b091-410ebc606e8e", "username": "anna_sales", "name": "Анна" },
    "first_message_sent_at": "2026-10-27T09:02:11.204518+00:00"
  }
}

Порядок ключей в JSON не гарантируется. Незнакомые поля пропускайте, а на событие незнакомого типа отвечайте 2xx, иначе оно будет повторяться. Подробнее в руководстве Версии API.

#Отличия от ленты

ПолеВебхукЛента GET /v1/events
objectНет"event"
data.event_idРавен idНет
data.lead.username_at, telegram_url, telegram_user_idЕстьНет
data.lead.username_variantsВосемь частых видов записи никаНет
data.lead.fieldsВсе колонки, загруженные с лидом, кроме телефона. Снимок на момент событияТолько перечисленные в руководстве Связь с CRM ключи, record_id из текущей карточки
data.lead.record_id_statusНетvalid, missing, conflict или unknown
data.lead.statusСтатус лида на момент сбора событияОдно из значений справочника, иначе unknown
data.reply.textЕсли включено «Передавать текст ответа»С include_text=true
Текст первого сообщенияНе передаетсяdata.first_message с include_text=true
data.reply.automation_stoppedНетВсегда null
Запись времениISO 8601 UTC со смещением +00:00, до микросекундISO 8601 UTC с Z и миллисекундами

Что важно знать о данных вебхука:

  • data.lead.fields содержит колонки файла, загруженного при импорте или при создании кампании. Имена латиницей в нижнем регистре, пробел заменяется на _: колонка Lead Source придет как lead_source. Телефон и служебные поля не передаются.
  • data.lead.fields.record_id приходит, если вы загрузили такую колонку. Проверки record_id_status в вебхуке нет: если значения нет или оно вызывает сомнение, сверьте лида запросом GET /v1/leads/{id}.
  • data.lead.status показывает статус в момент сбора события. Набор значений не зафиксирован: решения принимайте по type, а не по этому полю.
  • data.lead.username приходит в нижнем регистре, без @. Если ника нет, username и связанные поля равны null.
  • data.account равен null, если аккаунт отправителя не найден, например удален.
  • data.first_message_sent_at у события ответа берется из карточки лида и может быть null.

Текст ответа. Без «Передавать текст ответа» поля data.reply.text нет. С ним text содержит текст ответа. Если текст прочитать не удалось, text равен null, а text_unavailable равен true.

Размер. Если тело больше 256 КиБ, сначала убирается текст ответа (text равен null, text_truncated равен true), затем поля лида (fields пустой, fields_truncated равен true). Если и так много, событие не отправляется, а в журнале появляется «Событие слишком большое».

#Проверка подписи

Адрес вебхука может узнать посторонний и прислать на него поддельное событие. Подпись доказывает, что запрос отправила Meliora и тело не меняли. Подпись соответствует стандарту Standard Webhooks, поэтому подойдут и готовые библиотеки этого стандарта.

Ключ подписи имеет вид whsec_ и 44 знака base64. Храните его как секрет, например в переменной окружения, и не публикуйте в коде.

Как проверить:

  1. Возьмите сырое тело запроса, байты до разбора JSON.
  2. Соберите строку для подписи: значение webhook-id, точка, значение webhook-timestamp, точка, тело.
  3. Ключ HMAC: часть ключа после whsec_, раскодированная из base64.
  4. Посчитайте HMAC-SHA256 от строки и закодируйте результат в base64.
  5. Заголовок webhook-signature содержит одну или несколько подписей через пробел, каждая вида v1,<base64>. Событие подлинное, если ваш результат совпал хотя бы с одной подписью v1. Сравнивайте за постоянное время, а не обычным ==.
  6. Проверьте webhook-timestamp: стандарт рекомендует отклонять запросы, время которых отличается от вашего больше чем на 5 минут. Так перехваченный запрос нельзя прислать повторно позже. Часы сервера держите синхронизированными.
import crypto from 'node:crypto';
import express from 'express';

const TOLERANCE_SECONDS = 5 * 60;
// Текущий ключ; на время смены ключа можно перечислить два через запятую
const SECRETS = process.env.MELIORA_WEBHOOK_SECRETS.split(',');

export function verifyWebhook(rawBody, headers, secrets) {
  const id = headers['webhook-id'];
  const timestamp = headers['webhook-timestamp'];
  const signatures = headers['webhook-signature'];
  if (!id || !timestamp || !signatures) throw new Error('missing headers');

  const sentAt = Number(timestamp);
  if (!Number.isInteger(sentAt) || Math.abs(Date.now() / 1000 - sentAt) > TOLERANCE_SECONDS) {
    throw new Error('timestamp outside tolerance');
  }

  const signed = Buffer.concat([Buffer.from(`${id}.${timestamp}.`), rawBody]);
  const expected = secrets.map((secret) => {
    const key = Buffer.from(secret.replace(/^whsec_/, ''), 'base64');
    return Buffer.from(crypto.createHmac('sha256', key).update(signed).digest('base64'));
  });

  for (const part of signatures.split(' ')) {
    const [version, value = ''] = part.split(',');
    if (version !== 'v1') continue;
    const received = Buffer.from(value);
    if (expected.some((e) => e.length === received.length && crypto.timingSafeEqual(e, received))) {
      return JSON.parse(rawBody.toString('utf8'));
    }
  }
  throw new Error('no matching signature');
}

const app = express();

app.post('/meliora/webhook', express.raw({ type: 'application/json', limit: '1mb' }), async (req, res) => {
  let event;
  try {
    event = verifyWebhook(req.body, req.headers, SECRETS);
  } catch {
    return res.status(400).end();
  }
  await saveEvent(event); // запись с уникальным event.id; повтор просто пропускается
  res.status(200).end();  // обработка в CRM идет после ответа, отдельной задачей
});

saveEvent и save_event замените своими: например, вставкой в таблицу с уникальным ключом по id и отдельным обработчиком, который применяет события к CRM.

#Проверьте свою реализацию

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

Значение
Ключwhsec_AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=
webhook-idevt_aab3db461d0542ff88c3124491aa2cc4
webhook-timestamp1793092509
Тело, без перевода строки в конце{"id":"evt_aab3db461d0542ff88c3124491aa2cc4","type":"lead.replied","test":false}
Ожидаемая подписьv1,kt8BoIJwYXnW8z5nUWwQitssRUMxKam8Dv+TgAhDV0s=

Тело в примере укорочено, для проверки алгоритма этого достаточно.

#Ответ на запрос

Ответьте кодом 2xx в течение 10 секунд, тогда событие считается доставленным. Тело ответа не нужно, Meliora его не сохраняет.

Все остальное считается неудачной попыткой:

  • коды 3xx, 4xx и 5xx; переадресации не выполняются;
  • нет ответа за 10 секунд;
  • соединение и рукопожатие TLS не установились за 5 секунд;
  • ошибка DNS, соединения или сертификата.

Общий срок одной попытки 30 секунд. Ответьте сразу после того, как сохранили событие, а запись в CRM и другую долгую работу выполняйте после ответа.

Особые ответы:

ОтветЧто делает Meliora
410 GoneБольше не повторяет это событие и сразу выключает адрес. Отвечайте 410, только если адрес действительно больше не нужен.
413 Payload Too LargeБольше не повторяет это событие. Адрес продолжает работать.
429 или 503 с Retry-AfterПауза до следующей попытки равна большему из двух значений: паузе по расписанию и Retry-After в секундах. Значение больше часа считается за час. Дата в Retry-After не поддерживается, тогда действует расписание.

#Повторы

Если попытка не удалась, Meliora повторяет ее с тем же webhook-id и тем же телом. Всего до 10 попыток примерно за 3 суток. Паузы между попытками случайно отличаются от расписания до 10% в обе стороны.

ПопыткаПауза после предыдущейПримерно от первой попытки
1Сразу после сбора события
25 секунд5 секунд
35 минут5 минут
430 минут35 минут
52 часа2 ч 35 мин
65 часов7 ч 35 мин
710 часов17 ч 35 мин
814 часов31 ч 35 мин
920 часов51 ч 35 мин
1024 часа75 ч 35 мин

После 10 неудачных попыток событие получает статус «Не доставлено». Его можно отправить снова в «Журнале» адреса: кнопка «Повторить» у события или «Повторить неудачные за 7 дней». Повтор начинает новый цикл из 10 попыток с тем же id.

Событие, которое не удалось доставить за 7 суток с момента появления, например пока адрес на паузе, отменяется со статусом «Не доставлено за 7 суток». Такое событие тоже можно повторить из журнала. Журнал хранит доставки 30 дней.

#Автоматическое выключение адреса

Пока адрес не принимает события, карточка показывает «Есть ошибки». Адрес выключается автоматически:

  • сразу после ответа 410;
  • если ошибки идут больше 3 суток подряд и за это время было не меньше 10 неудачных попыток подряд. Любой ответ 2xx сбрасывает счетчик.

Выключенный адрес получает статус «Выключен». Новые события для него не копятся и не будут доставлены. Исправьте получателя и нажмите «Включить». События за время отключения заберите из ленты или сверкой лидов.

#Гарантии доставки

  • Не реже одного раза. Одно событие может прийти несколько раз, например если ваш ответ потерялся в сети. Дедуплицируйте по id (он же webhook-id) и на повтор отвечайте 2xx.
  • Без гарантии порядка. lead.replied может прийти раньше lead.first_message_sent. Ориентируйтесь на timestamp и обновляйте статусы в CRM только вперед, как описано в руководстве Связь с CRM.
  • Без гарантии срока. События собираются раз в несколько секунд и отправляются сразу после сбора. Часть событий находится позже, при сверке, и приходит с опозданием и с более ранним timestamp.
  • Сейчас на один адрес уходит не больше одного запроса одновременно. Не полагайтесь на это для порядка: повторы идут вперемешку с новыми событиями.

#Ротация ключа

Смените ключ, если он мог попасть к посторонним, или по своему графику.

  1. На карточке адреса нажмите «Ключ подписи», затем «Сменить ключ». Скопируйте новый ключ.
  2. Прежний ключ действует еще сутки. Все это время каждый запрос несет две подписи: новым и прежним ключом. Получатель с прежним ключом продолжает работать.
  3. В течение суток замените ключ в своей системе. Чтобы переход прошел без единой ошибки, на это время можно принимать оба ключа: код выше принимает список.

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

#Тестовое событие

Кнопка «Проверить» на карточке активного адреса отправляет тестовое событие. Нажимать ее можно не чаще раза в 10 секунд. Результат появляется в «Журнале».

Тестовое событие подписывается и повторяется так же, как настоящее. Отличия:

  • type равен webhook.test, test равен true;
  • data устроен как у lead.replied, а data.example_of равен "lead.replied";
  • идентификаторы в data случайные, ник example_lead;
  • в data.lead.fields приходят имена колонок ваших недавних лидов со значением "пример"; настоящие значения не передаются;
  • data.reply.text с примером текста приходит, только если включено «Передавать текст ответа».

По тестовому событию удобно настроить разбор полей, например в сервисе автоматизации. В рабочей обработке события с test: true пропускайте.

#Пауза, изменение и удаление

  • Пауза. Кнопка «Пауза» останавливает отправку. Новые события для адреса копятся, а после «Продолжить» уходят. Событие, которое ждет дольше 7 суток, отменяется, его можно повторить из журнала.
  • Изменение. Кнопка «Изменить» меняет адрес, события, охват, передачу текста и заголовки. Если вы сузили события или охват, ждущие отправки события, которые больше не подходят, отменяются со статусом «Адрес больше не принимает это событие».
  • Удаление. «Изменить», затем «Удалить адрес» и подтверждение «Удалить». Ждущие события не уйдут, ключи подписи удаляются.

#Журнал

Карточка адреса показывает, сколько событий доставлено и сколько ошибок за сутки, сколько ждут отправки и чем закончилась последняя отправка. Кнопка «Журнал» открывает список доставок со статусами «Ждет отправки», «Отправляется», «Доставлено», «Не доставлено» и «Отменено», кодом ответа, номером попытки и временем следующей. Фильтр «Ошибки и ожидание» оставляет только проблемные доставки.

Код 2xx в журнале значит, что ваш адрес принял запрос. Что с событием было дальше, видно только в журнале вашей системы.

#Частые ошибки

В журналеПричина и что делать
«Адрес не подходит»Адрес не https, другой порт или IP вместо имени. Исправьте адрес.
«Сервер не найден по имени»Имя не разрешается в DNS. Проверьте записи домена.
«Адрес ведет во внутреннюю сеть»Имя указывает на частный или служебный адрес. Нужен публичный адрес.
«Сервер не принял соединение»Порт 443 закрыт или запросы режет межсетевой экран. Откройте порт: адреса серверов Meliora не публикуются, разрешить их списком нельзя.
«Ошибка защищенного соединения»Сертификат истек, самоподписанный, выдан на другое имя или цепочка неполная.
«Сервер не ответил вовремя»Ответ дольше 10 секунд. Отвечайте сразу после сохранения события.
«Сервер ответил ошибкой» с кодом 3xxПереадресация, например с http на https или на адрес со слешем в конце. Укажите окончательный адрес.
«Сервер ответил ошибкой» с кодом 401 или 403Ваш сервис не принял ключ. Проверьте «Дополнительные заголовки».
«Сервер ответил ошибкой» с кодом 400Часто это ваша проверка подписи. Смотрите строку ниже.
«Событие слишком большое»Тело больше 256 КиБ даже без текста и полей лида. Напишите в поддержку.

Если подпись не сходится, проверьте по порядку: подпись считается от сырого тела, а не от заново собранного JSON; используется ключ этого адреса и вся часть после whsec_ раскодирована из base64; ключ не меняли кнопкой «Сменить ключ»; часы вашего сервера идут верно. Сверьте свой код с примером.

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