# Вебхуки

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

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

Вебхуки и [лента событий](https://docs.melioralab.io/events-polling/) передают одни и те же факты: `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`.

Адрес получает события, собранные после того, как его сохранили. Прошлые события на новый адрес не отправляются: историю заберите из [ленты](https://docs.melioralab.io/events-polling/) или обходом [лидов](https://docs.melioralab.io/reference/leads/).

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

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

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

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

> **Важно.** Вебхуки сейчас в раннем доступе. Если блока «Вебхук событий» нет в настройках, напишите в [поддержку](https://docs.melioralab.io/support/), и мы подключим ваш кабинет.

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

- Только `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-Type` | `application/json` |
| `User-Agent` | `Meliora-Webhooks/1` |

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

### Тело

| Поле | Описание |
|---|---|
| `id` | Идентификатор события, ключ дедупликации. Тот же, что у события в [ленте](https://docs.melioralab.io/events-polling/). |
| `type` | `lead.first_message_sent`, `lead.replied` или `webhook.test` у тестового события. |
| `api_version` | Версия формата события, сейчас `2026-10-18`. Подробнее в руководстве [Версии API](https://docs.melioralab.io/versioning/). |
| `timestamp` | Когда произошел факт. Это не время отправки: повтор приходит с тем же `timestamp`. |
| `test` | `true` только у тестового события. |
| `data` | Данные события. |

Общие поля `data` описаны в справочнике [событий](https://docs.melioralab.io/reference/events/): пространство, кампания, лид, аккаунт отправителя, время первого сообщения и ответ. Ниже примеры и отличия вебхука от ленты.

```json
{
  "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
{
  "id": "evt_aab3db461d0542ff88c3124491aa2cc4",
  "type": "lead.replied",
  "api_version": "2026-10-18",
  "timestamp": "2026-10-27T09:15:02.481233+00:00",
  "test": false,
  "data": {
    "event_id": "evt_aab3db461d0542ff88c3124491aa2cc4",
    "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": "replied",
      "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",
    "reply": {
      "id": "d3c448da-10fe-4493-ad22-55e1eb36478a",
      "replied_at": "2026-10-27T09:15:02.481233+00:00",
      "text": "Здравствуйте! Интересно, расскажите подробнее"
    }
  }
}
```

Порядок ключей в JSON не гарантируется. Незнакомые поля пропускайте, а на событие незнакомого типа отвечайте 2xx, иначе оно будет повторяться. Подробнее в руководстве [Версии API](https://docs.melioralab.io/versioning/#ustoychivyy-klient).

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

| Поле | Вебхук | Лента `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](https://docs.melioralab.io/crm-mapping/#polya) ключи, `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}`](https://docs.melioralab.io/reference/leads/).
- `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](https://www.standardwebhooks.com/), поэтому подойдут и готовые библиотеки этого стандарта.

Ключ подписи имеет вид `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 минут. Так перехваченный запрос нельзя прислать повторно позже. Часы сервера держите синхронизированными.

> **Внимание.** Не разбирайте и не собирайте JSON заново перед проверкой. Другой порядок ключей, пробелы или экранирование кириллицы дадут другие байты, и подпись не совпадет. Во фреймворке получите тело как байты, например `express.raw()` вместо `express.json()`.

```js
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 идет после ответа, отдельной задачей
});
```
```python
import base64
import hashlib
import hmac
import json
import os
import time

from fastapi import FastAPI, Request, Response

TOLERANCE_SECONDS = 5 * 60
# Текущий ключ; на время смены ключа можно перечислить два через запятую
SECRETS = os.environ["MELIORA_WEBHOOK_SECRETS"].split(",")

class InvalidWebhook(Exception):
    pass

def verify_webhook(raw_body: bytes, headers, secrets: list[str]) -> dict:
    msg_id = headers.get("webhook-id")
    timestamp = headers.get("webhook-timestamp")
    signatures = headers.get("webhook-signature")
    if not msg_id or not timestamp or not signatures:
        raise InvalidWebhook("missing headers")
    try:
        sent_at = int(timestamp)
    except ValueError:
        raise InvalidWebhook("bad timestamp")
    if abs(time.time() - sent_at) > TOLERANCE_SECONDS:
        raise InvalidWebhook("timestamp outside tolerance")

    signed = f"{msg_id}.{timestamp}.".encode() + raw_body
    expected = []
    for secret in secrets:
        key = base64.b64decode(secret.removeprefix("whsec_"))
        digest = hmac.new(key, signed, hashlib.sha256).digest()
        expected.append(base64.b64encode(digest))

    for part in signatures.split(" "):
        version, _, value = part.partition(",")
        if version != "v1":
            continue
        if any(hmac.compare_digest(value.encode(), e) for e in expected):
            return json.loads(raw_body)
    raise InvalidWebhook("no matching signature")

app = FastAPI()

@app.post("/meliora/webhook")
async def meliora_webhook(request: Request):
    raw_body = await request.body()
    try:
        event = verify_webhook(raw_body, request.headers, SECRETS)
    except InvalidWebhook:
        return Response(status_code=400)
    save_event(event)  # запись с уникальным event["id"]; повтор просто пропускается
    return Response(status_code=200)  # обработка в CRM идет после ответа
```

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

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

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

| | Значение |
|---|---|
| Ключ | `whsec_AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=` |
| `webhook-id` | `evt_aab3db461d0542ff88c3124491aa2cc4` |
| `webhook-timestamp` | `1793092509` |
| Тело, без перевода строки в конце | `{"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 | Сразу после сбора события | |
| 2 | 5 секунд | 5 секунд |
| 3 | 5 минут | 5 минут |
| 4 | 30 минут | 35 минут |
| 5 | 2 часа | 2 ч 35 мин |
| 6 | 5 часов | 7 ч 35 мин |
| 7 | 10 часов | 17 ч 35 мин |
| 8 | 14 часов | 31 ч 35 мин |
| 9 | 20 часов | 51 ч 35 мин |
| 10 | 24 часа | 75 ч 35 мин |

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

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

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

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

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

Выключенный адрес получает статус «Выключен». Новые события для него не копятся и не будут доставлены. Исправьте получателя и нажмите «Включить». События за время отключения заберите из [ленты](https://docs.melioralab.io/events-polling/) или сверкой [лидов](https://docs.melioralab.io/reference/leads/).

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

- **Не реже одного раза.** Одно событие может прийти несколько раз, например если ваш ответ потерялся в сети. Дедуплицируйте по `id` (он же `webhook-id`) и на повтор отвечайте 2xx.
- **Без гарантии порядка.** `lead.replied` может прийти раньше `lead.first_message_sent`. Ориентируйтесь на `timestamp` и обновляйте статусы в CRM только вперед, как описано в руководстве [Связь с CRM](https://docs.melioralab.io/crm-mapping/#statusy-v-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 КиБ даже без текста и полей лида. Напишите в [поддержку](https://docs.melioralab.io/support/). |

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

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