Вебхуки
События API Lalexi на ваш адрес: какие бывают, что приходит в теле, как проверить подпись, повторы и автоотключение.
Вебхук — это ваш адрес (HTTPS), на который Lalexi отправляет POST при
событии: пришло сообщение, изменился статус доставки, диалог закрыли, канал
отключился. Так интеграции не нужно опрашивать API.
Вебхук принадлежит человеку — тому, чьим токеном он создан, — и получает только то, что этот человек видит в Lalexi: оператор — события своих каналов, администратор — всего пространства. Участник, которого удалили из пространства, больше ничего не получает.
Как подключить
curl https://app.lalexi.com/v1/webhooks \
-H "Authorization: Bearer <ваш токен>" \
-H "Content-Type: application/json" \
-d '{"url": "https://crm.example.com/hooks/lalexi", "events": ["message.created", "conversation.updated"]}'В ответе — secret вида whsec_…. Он показывается один раз: сохраните его,
им вы будете проверять подпись. Новый секрет выдаёт
PATCH /webhooks/{id} с {"rotate_secret": true} — старый сразу перестаёт
подписывать.
- Адрес — только
https://в открытом интернете; частные и локальные адреса не принимаются. - До 10 вебхуков на человека в пространстве.
POST /webhooks/{id}/testприсылает тестовое событиеping— подписанное и с теми же повторами, что и настоящие.- Свои вебхуки:
GET /webhooks; изменить адрес или события —PATCH /webhooks/{id}; удалить —DELETE /webhooks/{id}.
События
| Событие | Когда приходит | Что в data |
|---|---|---|
message.created | В видимом вам диалоге появилось сообщение: входящее, исходящее (из Lalexi или с подключённого телефона) или заметка. Служебные строки («закрыт …») не приходят. | message, conversation |
message.updated | У сообщения изменился статус доставки (delivered, read, failed с error_code) или текст, либо его удалили. | message, conversation |
conversation.created | Появился новый видимый вам диалог. | conversation |
conversation.updated | Изменились статус, приоритет, ответственный, команда, метки, поля или время «отложен до». | conversation, changed |
contact.created | Создан контакт. | contact |
contact.updated | Изменились имя, e-mail, телефон, идентификатор, поля или блокировка контакта. | contact, changed |
channel.created | Подключён новый номер — через POST /channels или в Lalexi. Приходит тому, кто его подключил, и тем, кто работает в канале. | channel |
channel.status_changed | Изменился статус подключения канала мессенджера, в котором вы работаете. | channel, previous_status, status |
ping | Тестовая доставка из POST /webhooks/{id}/test. На неё не подписываются. | message |
changed — список изменившихся полей объекта, например ["status", "assignee"].
Одно изменение — одна доставка. Объекты в data — те же, что отдаёт API
(GET /conversations/{id}, GET /contacts/{id}, GET /channels/{id}); их
поля описаны в справочнике.
Что приходит
POST /hooks/lalexi HTTP/1.1
Content-Type: application/json
User-Agent: Lalexi-Webhooks/1
X-Lalexi-Event: message.created
X-Lalexi-Delivery: 6f1c2b9e-1d2a-4c55-9d7e-3f0a8b7c6d5e
X-Lalexi-Timestamp: 1793093400
X-Lalexi-Signature: sha256=9dfbf0a43511ead4b14f1cd9594bf54a94e01783962dc739a63e0f13d19461ed{
"id": "6f1c2b9e-1d2a-4c55-9d7e-3f0a8b7c6d5e",
"event": "message.created",
"created_at": "2026-10-27T09:30:00Z",
"workspace_id": 7,
"data": {
"message": {
"id": 88213,
"conversation_id": 1042,
"type": "incoming",
"text": "Здравствуйте! Заказ уже отправили?",
"deleted": false,
"sender": { "type": "contact", "id": 5120, "name": "Анна Петрова" },
"status": "sent",
"error_code": null,
"attachments": [],
"created_at": "2026-10-27T09:30:00Z",
"updated_at": "2026-10-27T09:30:00Z"
},
"conversation": {
"id": 1042,
"status": "open",
"priority": null,
"channel": { "id": 12, "name": "Продажи", "type": "whatsapp" },
"contact": { "id": 5120, "name": "Анна Петрова", "phone": "+79001234567", "email": null },
"assignee": { "id": 3, "name": "Мария" },
"team": null,
"labels": ["заказ"],
"custom_attributes": {},
"snoozed_until": null,
"created_at": "2026-10-20T08:00:00Z",
"updated_at": "2026-10-27T09:30:00Z",
"last_activity_at": "2026-10-27T09:30:00Z"
}
}
}| Заголовок | Что это |
|---|---|
X-Lalexi-Event | Событие, то же, что event в теле. |
X-Lalexi-Delivery | Номер доставки, тот же, что id в теле. Не меняется при повторах — по нему отсеивайте дубли. |
X-Lalexi-Timestamp | Когда подписана эта попытка, в секундах Unix. Входит в подпись. |
X-Lalexi-Signature | sha256= и подпись в hex. |
Проверка подписи
Проверяйте каждую доставку, прежде чем ей верить:
- возьмите заголовок
X-Lalexi-Timestampи сырое тело запроса — байты, как они пришли, до разбора JSON; - посчитайте HMAC-SHA256 от строки
{timestamp}.{тело}, ключ — весь секрет целиком, вместе сwhsec_; результат в hex; - сравните с тем, что после
sha256=вX-Lalexi-Signature, сравнением за постоянное время; - отклоните доставку, если время подписи отличается от ваших часов больше чем на 5 минут, — это повтор старого запроса.
import crypto from 'node:crypto';
import http from 'node:http';
const secret = process.env.LALEXI_WEBHOOK_SECRET; // whsec_…
function verify(rawBody, timestamp, signature) {
const age = Math.abs(Date.now() / 1000 - Number(timestamp));
if (!timestamp || !(age <= 300)) return false;
const expected =
'sha256=' +
crypto.createHmac('sha256', secret).update(`${timestamp}.`).update(rawBody).digest('hex');
const a = Buffer.from(expected);
const b = Buffer.from(signature ?? '');
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
http
.createServer((req, res) => {
const chunks = [];
req.on('data', (chunk) => chunks.push(chunk));
req.on('end', () => {
const raw = Buffer.concat(chunks);
if (!verify(raw, req.headers['x-lalexi-timestamp'], req.headers['x-lalexi-signature'])) {
res.writeHead(401).end();
return;
}
const delivery = JSON.parse(raw);
// Отсейте дубль по delivery.id и поставьте событие в свою очередь.
console.log(delivery.event, delivery.id);
res.writeHead(204).end();
});
})
.listen(3000);import hashlib
import hmac
import os
import time
from flask import Flask, abort, request
app = Flask(__name__)
SECRET = os.environ["LALEXI_WEBHOOK_SECRET"].encode() # whsec_…
def verify(body: bytes, timestamp: str, signature: str) -> bool:
if not timestamp.isdigit() or abs(time.time() - int(timestamp)) > 300:
return False
expected = "sha256=" + hmac.new(SECRET, timestamp.encode() + b"." + body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature)
@app.post("/hooks/lalexi")
def lalexi_webhook():
body = request.get_data() # сырые байты, до разбора JSON
if not verify(body, request.headers.get("X-Lalexi-Timestamp", ""), request.headers.get("X-Lalexi-Signature", "")):
abort(401)
delivery = request.get_json()
# Отсейте дубль по delivery["id"] и поставьте событие в свою очередь.
print(delivery["event"], delivery["id"])
return "", 204Не пересобирайте JSON перед проверкой: после разбора и обратной сборки байты другие, и подпись не сойдётся.
Ответ, повторы и автоотключение
- Ответьте любым кодом 2xx в течение 5 секунд (доставка целиком, с редиректами, обрывается через 15 секунд). Тело ответа не читается. Тяжёлую обработку делайте после ответа — в своей очереди.
- Всё остальное — неудачная попытка: другой код, тайм-аут, обрыв соединения. Доставку повторят с растущими паузами — от секунд до нескольких минут, всего до 5 попыток.
- После 50 неудачных попыток подряд (повторы считаются) вебхук
выключается:
active: false. Включите его снова черезPATCH /webhooks/{id}с{"active": true}— счётчик неудач обнулится. Пока вебхук выключен, события на него не копятся. - Итог последней попытки виден в
GET /webhooks/{id}:last_delivery_at,last_status,last_error,consecutive_failures. - Порядок не гарантирован, а доставка изредка приходит дважды. Отсеивайте
дубли по
X-Lalexi-Delivery(idв теле), а порядок определяйте поupdated_atобъектов.