Webhook в MAX Bot API: как получать события и не утонуть в дублях
Разбираем webhook в MAX Bot API по-человечески: подписка, secret, типы событий, быстрый ответ сервером и защита от повторной доставки.
Зачем вообще нужен webhook
Если вы строите не игрушечного бота, а рабочий продукт, webhook почти всегда нужен раньше, чем сложная аналитика. Именно он превращает бота из “мы умеем отправлять текст” в “мы понимаем, что произошло после отправки”.
Через webhook вы получаете входящие сообщения, нажатия на кнопки и другие события от платформы. Для команды это самый быстрый способ увидеть полный цикл: пользователь написал, ваш сервер получил событие, бизнес-логика сработала, ответ ушёл обратно.
Как подписаться на события
В документации MAX для webhook используется POST /subscriptions. У подписки есть URL и секрет, который потом приходит в заголовке X-Max-Bot-Api-Secret. Это стоит включать сразу, даже если проект пока маленький: так вы хотя бы не принимаете любой POST “как будто от MAX”.
curl -X POST "https://platform-api.max.ru/subscriptions" \
-H "Authorization: Bearer YOUR_MAX_BOT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-app.example.com/webhook/max",
"secret": "replace-with-a-long-random-secret"
}'В официальной документации также зафиксировано: сервер должен ответить на webhook не позднее чем через 30 секунд. Если ответа нет, MAX повторяет доставку. Документация указывает, что число повторов может доходить до десяти, поэтому проектировать handler как “один запрос = одно событие навсегда” опасно.
Какие события реально приходят
В официальном описании обновлений у MAX есть несколько базовых событий, которые покрывают большинство первых сценариев:
- message_created — пришло новое сообщение от пользователя.
- message_callback — пользователь нажал callback-кнопку.
- bot_started — бот был запущен пользователем, например через deep link.
{
"update_type": "message_created",
"timestamp": 1710412800,
"message": {
"recipient": { "chat_id": 1234567890 },
"body": { "mid": "msg_001", "text": "Хочу узнать статус заказа" },
"sender": { "user_id": 42, "name": "Алиса" }
}
}На практике этого уже достаточно, чтобы подключить базовую поддержку, FAQ-сценарии и операционный таймлайн клиента. Не нужно ждать идеальной event model, чтобы начать работать.
Дубли, retries и idempotency
Вот где чаще всего ломается “быстрый запуск”. Команда обрабатывает webhook как обычный HTTP POST, вешает на него тяжёлую бизнес-логику и получает дубли при первом же таймауте. Виноват обычно не MAX, а сам подход.
- Проверяйте секрет из заголовка
X-Max-Bot-Api-Secret. - Возвращайте
200 OKмаксимально быстро. - Дальше кладите событие в очередь и обрабатывайте асинхронно.
- Храните ключ идемпотентности по событию или по комбинации
update_type + mid.
app.post("/webhook/max", async (req, res) => {
if (req.headers["x-max-bot-api-secret"] !== process.env.MAX_WEBHOOK_SECRET) {
return res.status(401).json({ ok: false });
}
const event = req.body;
const key = `${event.update_type}:${event.message?.body?.mid ?? event.callback?.callback_id ?? "unknown"}`;
if (await store.has(key)) {
return res.status(200).json({ ok: true, duplicate: true });
}
await store.put(key);
await queue.add("max-events", event);
return res.status(200).json({ ok: true });
});Именно этот паттерн спасает от повторной отправки сообщений, дублей тикетов и лишних side effects в CRM.
Минимальный сервер
Для первого рабочего webhook не нужен сложный фреймворк. Нужен аккуратный контур: валидация секрета, быстрый ответ, логирование и фоновая обработка.
import express from "express";
const app = express();
app.use(express.json());
app.post("/webhook/max", async (req, res) => {
const secret = req.header("x-max-bot-api-secret");
if (secret !== process.env.MAX_WEBHOOK_SECRET) {
return res.status(401).json({ ok: false });
}
const update = req.body;
res.status(200).json({ ok: true });
await eventsQueue.add(update);
});
app.listen(3000);Дальше у вас уже есть пространство для роста: маршрутизация в CRM, операторы, триггеры, аналитика. Но до этого момента можно не усложнять.
Когда хватит long polling
Если вы локально отлаживаете бота или хотите быстро посмотреть структуру событий, GET /updatesабсолютно нормален. Но как только бот становится частью продукта, webhook выигрывает почти всегда:
| Подход | Плюс | Минус |
|---|---|---|
| Long polling | Просто начать на локалке | Плохо масштабируется для постоянной серверной интеграции |
| Webhook | Лучше ложится в продукт и CRM | Нужны HTTPS и дисциплина по идемпотентности |
Если хотите сравнить два пути подробнее, есть отдельный разбор Webhook или long polling в MAX Bot API. А когда webhook уже работает, следующим узким местом чаще всего становится не приём событий, а 429 и контроль темпа отправки.
Хороший webhook это не “сложная интеграция”, а один скучный, но надёжный кусок инфраструктуры. Чем меньше магии вы в него закладываете, тем спокойнее он переживает продакшен.
Создайте бесплатный MAX-профиль
Если хочется не просто читать, а сразу проверить сценарий руками: подключите MAX, отправьте себе тестовое сообщение и уже потом решайте, нужны ли другие каналы.