Документация
Вебхуки
Evidra отправляет событие на ваш адрес подписанным запросом. Опрос API по расписанию не нужен.
Подключение
Адрес задаётся в кабинете: Разработчикам → Вебхуки. При добавлении выбираются события и выдаётся секрет подписи. Секрет показывается один раз, как и ключ API. На организацию можно завести до 5 адресов, например прод и тест.
Адрес только с https и с публичным хостом. На localhost и адреса внутренних сетей запрос не уходит. Для локальной отладки используйте туннель, например ngrok.
Как выглядит запрос
POST с телом application/json. Конверт одинаковый для всех событий, различается только data:
{
"event": "cascade.recipient.finished",
"createdAt": "2026-08-30T18:20:11.000Z",
"organizationId": "clx…",
"data": { … }
}Заголовки, которые приходят с каждым событием:
| Заголовок | Назначение |
|---|---|
X-Evidra-Event | Ключ события — тот же, что в теле. |
X-Evidra-Delivery | Идентификатор доставки. Повторы приходят с тем же значением — используйте для дедупликации. |
X-Evidra-Timestamp | Unix-время подписи в секундах. Входит в подписываемую строку. |
X-Evidra-Signature | Подпись v1=<HMAC-SHA256>, см. ниже. |
Ответ и повторы
Ответьте кодом 2xx в течение 10 секунд. Тяжёлую работу поставьте в очередь после ответа. Другой код или молчание считаются неудачей. Evidra повторит доставку ещё 5 раз. Пауза растёт: через минуту, 5 минут, полчаса, 2 часа, 6 часов, сутки.
После 10 неудачных доставок адрес выключается. Причина видна в кабинете. Включите адрес после починки. Пропущенные события повторно не отправляются.
Проверка подписи
Адрес публичный. Без проверки подписи на него может прийти запрос не от Evidra. Заголовок X-Evidra-Signature содержит v1=<hex>, где hex — HMAC-SHA256 от строки {X-Evidra-Timestamp}.{сырое тело запроса} на секрете эндпоинта.
import crypto from "node:crypto";
// express.raw, а не express.json: подпись считается по сырому телу.
// Пересобранный из объекта JSON даст другие байты, и проверка не сойдётся.
app.post("/hooks/evidra", express.raw({ type: "application/json" }), (req, res) => {
const timestamp = req.get("X-Evidra-Timestamp") ?? "";
const signature = req.get("X-Evidra-Signature") ?? "";
const body = req.body.toString("utf8");
const expected =
"v1=" +
crypto
.createHmac("sha256", process.env.EVIDRA_WEBHOOK_SECRET)
.update(`${timestamp}.${body}`)
.digest("hex");
const a = Buffer.from(signature);
const b = Buffer.from(expected);
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
return res.sendStatus(401);
}
// Отвечаем сразу, разбор — в фон: дольше 10 секунд мы не ждём.
res.sendStatus(200);
handleEvent(JSON.parse(body));
});Два правила сверх самой подписи:
- Отклоняйте запрос, если
X-Evidra-Timestampстарше 5 минут. Подпись не протухает. Старый запрос иначе можно отправить снова. - Повтор приходит с тем же
X-Evidra-Delivery. Храните обработанные идентификаторы и пропускайте дубли.
Секрет можно перевыпустить в кабинете. Старая подпись перестаёт проходить сразу. Сначала сохраните новый секрет у себя, затем перевыпускайте его в кабинете.
События
Подписка настраивается по событиям. На адрес приходят только выбранные. Ниже указано, что означает каждое, и приведён пример тела.
Каскад по получателю завершёнcascade.recipient.finished
Каскад для конкретного клиента остановлен. Смотрите finishReason: RESPONDED — клиент поставил оценку (в rating), COMPLETED — шаги кончились без ответа, SUPPRESSED — клиент отписан, NO_CONTACT — не нашлось пригодного контакта.
{
"event": "cascade.recipient.finished",
"createdAt": "2026-08-30T18:20:11.000Z",
"organizationId": "clx2222222222222222222222",
"data": {
"cascadeId": "clx0000000000000000000000",
"externalId": "order-10423",
"locationId": "clx1111111111111111111111",
"recipient": {
"name": "Анна",
"email": "anna@example.com",
"phone": "79123456789"
},
"finishReason": "RESPONDED",
"rating": 5,
"respondedAt": "2026-08-30T18:20:11.000Z",
"attempts": [
{
"step": 1,
"channel": "EMAIL",
"status": "SENT",
"at": "2026-08-29T09:00:00.000Z"
}
]
}
}Сообщение доставленоmessage.delivered
Шлюз подтвердил доставку сообщения получателю. Приходит по данным площадки доставки, поэтому может отставать от факта отправки на несколько минут.
{
"event": "message.delivered",
"createdAt": "2026-08-30T18:20:11.000Z",
"organizationId": "clx2222222222222222222222",
"data": {
"cascadeId": "clx0000000000000000000000",
"externalId": "order-10423",
"recipient": {
"name": "Анна",
"email": "anna@example.com",
"phone": "79123456789"
},
"channel": "EMAIL",
"providerMessageId": "184623901",
"at": "2026-08-29T09:00:42.000Z"
}
}Сообщение не доставленоmessage.failed
Шаг каскада не ушёл или был отбит получателем/шлюзом. Это НЕ конец каскада: следующий шаг по другому каналу всё ещё может сработать — окончание смотрите по cascade.recipient.finished.
{
"event": "message.failed",
"createdAt": "2026-08-30T18:20:11.000Z",
"organizationId": "clx2222222222222222222222",
"data": {
"cascadeId": "clx0000000000000000000000",
"externalId": "order-10423",
"recipient": {
"name": "Анна",
"email": "anna@example.com",
"phone": "79123456789"
},
"channel": "EMAIL",
"reason": "BOUNCED",
"error": "mailbox not found",
"at": "2026-08-29T09:01:03.000Z"
}
}