Важно: API и интерфейсы MAX развиваются. Перед продакшеном сверьте каждый пример — формат заголовка Authorization, поля объекта Update, список заголовков вебхука — по официальной документации. Если статья расходится с dev.max.ru, приоритет у платформы.
Для кого эта статья
Если вы впервые подключаете бота к мессенджеру MAX и видите слова «вебхук», «endpoint» и «API» — ниже сначала коротко объясним смысл, затем разберём эхо-бот: он отвечает тем же текстом, который вы ему написали. Так проще понять цепочку: MAX → ваш сервер → снова MAX. Словарик в двух фразах- Вебхук (webhook) — это когда MAX сам присылает уведомление на ваш адрес в интернете (обычно POST с JSON). Вам не нужно постоянно опрашивать API вопросом «есть ли новые сообщения?».
- Endpoint — URL вашей программы, например
https://bot.example.com/webhook, куда MAX шлёт эти POST-запросы. - Подписка (в API MAX) — регистрация у платформы этого endpoint: «шли сюда события бота». Подробно — в разделе «Подписки» ниже.
Готовый пример: репозиторий и результат в MAX
Актуальный код эхо-бота (Flask, вебхук, проверкаX-Max-Bot-Api-Secret, вызов POST /messages с user_id / chat_id в query по типу чата recipient.chat_type, деплой на Bothost) выложен в репозитории bothost-tech/bothost_webhook-max — в README описаны переменные окружения платформы и пошаговая проверка.
После того как бот доступен по HTTPS и вы зарегистрировали URL вебхука в MAX (это и есть подписка — см. раздел ниже), диалог в MAX может выглядеть так:
Зачем боту вебхук и как быть с long polling
Чтобы бот реагировал на сообщения, кнопки и другие действия, сервер должен получать обновления (объект Update). По позиции MAX, long polling (GET /updates) ограничен по скорости и сроку хранения событий и не подходит для production; для продуктовых интеграций рекомендуется вебхук — см. «События» и раздел «Подписки» ниже (метод POST /subscriptions). Это не значит, что long polling «плох»: для локальной отладки, быстрого просмотра JSON и сценария без публичного HTTPS он часто удобнее вебхука. Когда появится стабильный HTTPS на 443, переключайтесь на вебхук.
Важно: вебхук и long polling нельзя включить одновременно — при активной подписке на вебхук long polling отключается (FAQ).
Подписки: что это и как связаны с доменом
Слово «подписка» в MAX Bot API не про оплату и не про подписку пользователя на канал. Это настройка у платформы MAX: «для моего бота отправляй уведомления о событиях на вот этот адрес в интернете».Что такое подписка на вебхук — одной мыслью
Подписка на вебхук — это указание MAX единственного (актуального) URL, куда облако будет само присылать HTTP-запросы с телом события (JSON с Update): новое сообщение, нажатие кнопки и т.д. Пока этот URL не зарегистрирован через API, для MAX ваш сервер «вне игры»: бот может крутиться на домене круглосуточно, но входящих событий по вебхуку не будет — платформа просто не знает, куда их доставлять. По смыслу это похоже на адрес доставки в службе доставки: вы получили домен и подняли сайт — это ещё не «подписка». Подписка — это отдельный шаг: вы сообщаете MAX полный HTTPS-адрес вебхука, напримерhttps://ваш-домен/webhook, и с этого момента туда и пойдут POST-уведомления.
От домена к потоку событий
Разложим по шагам, чтобы не путаться в терминах:- Домен и HTTPS — вы вывели бота в интернет: есть имя хоста (домен или поддомен) и валидный TLS на 443. Без этого MAX вебхук на ваш URL не повесит (требования к URL — в разделе ниже «Требования к адресу вебхука»).
- Путь вебхука — внутри домена вы выбрали конкретный путь, куда ваш код принимает POST (часто
/webhook). Вместе с протоколом и доменом это и есть полный URL вебхука. - Подписка — вы вызываете API MAX и передаёте этот полный URL в поле
url. Формально это делает метод POST /subscriptions: вы как бы говорите платформе: «все выбранные типы событий слать сюда». Это и есть подписка на вебхук в смысле документации: не «рассылка на email», а привязка доставки событий к вашему endpoint. - Дальше — при новом сообщении MAX сам делает POST на ваш URL; ваш код отвечает 200 и при необходимости вызывает исходящий POST /messages, чтобы ответить пользователю в чате.
Проверить, что MAX запомнил адрес
Текущую конфигурацию (какойurl и какие update_types сейчас сохранены) можно посмотреть запросом GET /subscriptions с тем же токеном в Authorization. Повторный POST /subscriptions обычно заменяет предыдущую подписку новой — то есть «переезд» бота на другой URL делается новым POST.
Регистрация подписки в API: POST /subscriptions
Чтобы создать или обновить подписку программно, вызываютPOST https://platform-api.max.ru/subscriptions. В заголовке Authorization передаётся токен бота (из кабинета MAX). В теле JSON — как минимум url (ваш вебхук), при желании secret и список update_types.
Заголовок Authorization: с Bearer или без?
В официальном примере в документации MAX указаноAuthorization: {access_token} — то есть без префикса Bearer (POST /subscriptions, GET /me). Ориентируйтесь на это как на канон для MAX.
Если ваш клиент или прокси при таком формате возвращает 401, проверьте токен и попробуйте вариант по RFC 6750: Authorization: Bearer <токен> — в экосистемах встречаются оба стиля; рабочий вариант лучше зафиксировать тестом GET /me перед подпиской.
Пример подписки (подставьте токен, URL и секрет):
curl -X POST "https://platform-api.max.ru/subscriptions" \
-H "Authorization: ВАШ_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-domain.com/webhook",
"update_types": ["message_created", "bot_started"],
"secret": "your_webhook_secret"
}'url— HTTPS, порт в URL не пишется (снаружи 443).secret— строка 5–256 символов в разрешённом наборе (см. доку); MAX присылает её вX-Max-Bot-Api-Secret. Значение в подписке и в коде должно совпадать байт в байт с тем же именем переменной окружения (без лишних пробелов и переводов строк).update_types— фильтр событий; полный список — в Update.
curl.exe); разбор ответа GET см. в подразделе «Проверить, что MAX запомнил адрес».
В cmd или PowerShell вызывайте именно curl.exe из системы (например C:\Windows\System32\curl.exe), а не алиас curl в PowerShell — он часто указывает на Invoke-WebRequest и ломает те же флаги.
-sS— не показывать прогресс-бар, но не глушить ошибки сети и TLS (-Sобязателен вместе с-s, если нужна диагностика).Authorization— токен бота в том же виде, с которым у вас уже проходитGET /me(см. выше: сBearerили без — как принято у MAX для вашего окружения).- В
-dJSON в двойных кавычках с экранированием\"внутри — удобный вариант для одной строки в Windows;url,secretи списокupdate_typesдолжны совпадать с тем, что вы задали в коде и в переменных окружения (WEBHOOK_URL,WEBHOOK_SECRET).
ВАШ_ТОКЕН, свой HTTPS-URL вместо примера на Bothost и свой WEBHOOK_SECRET вместо плейсхолдера:
curl.exe -sS -X POST "https://platform-api.max.ru/subscriptions" -H "Authorization: ВАШ_ТОКЕН" -H "Content-Type: application/json" -d "{\"url\":\"https://bot-1778625434-3168-alex.bothost.tech/webhook\",\"update_types\":[\"message_created\"],\"secret\":\"WEBHOOK_SECRET\"}"GET /subscriptions без тела; в ответе сверьте url, update_types и что платформа видит активную конфигурацию (детали полей — в актуальной документации MAX):
curl.exe -sS "https://platform-api.max.ru/subscriptions" -H "Authorization: ВАШ_ТОКЕН"POST вернул ошибку, сначала исправьте токен, JSON или требования к url (HTTPS на 443), затем снова GET — так вы отделяете проблему «подписка не принята» от «вебхук не отвечает 200».
Токен: business.max.ru — «Чат-боты» → «Интеграция» → «Получить токен» (названия в интерфейсе могут меняться).
Требования к адресу вебхука
По POST /subscriptions:- Только HTTPS на 443; самоподписанный сертификат не подходит.
- Домен в URL совпадает с CN/SAN сертификата; отдаётся полная цепочка сертификатов.
- Ответ
HTTP 200в течение 30 секунд. - Ошибки доставки — до 10 повторов с растущей паузой; 8 часов без успеха — автоотписка.
Безопасность вебхука: secret и границы модели
Документация MAX описывает проверку заголовкаX-Max-Bot-Api-Secret, если при подписке задан параметр secret. Это отсекает случайные запросы на URL и снижает риск простого сканирования.
Важно: это не замена полному моделированию угроз. Если платформа позже документирует, например, подпись тела запроса (аналог HMAC от сырого body), следуйте новой спецификации на dev.max.ru. В текущем разделе подписки не описан заголовок вроде X-Max-Bot-Api-Signature — не стоит копировать примеры из других сервисов (Stripe, GitHub) без проверки по документации MAX.
Дополнительно (если доступно в вашей инфраструктуре): ограничение по IP, отдельный путь только для MAX, rate limit на уровне nginx.
Правила платформы (кратко)
- Токен только в
Authorization, не в query (документация). secretиX-Max-Bot-Api-Secret— пара «подписка ↔ код».- Ориентир нагрузки на
platform-api.max.ru— порядка ~30 rps; при 429 — паузы и backoff (точные лимиты смотрите в актуальной документации).
События update_types и bot_started
Список типов — в Update. Для эхо достаточно message_created.
bot_started по смыслу официального описания — когда пользователь впервые начал общение с ботом или возобновил после остановки (кнопка в настройках MAX). То есть это не «строго один раз за всю жизнь аккаунта»: при повторном «запуске» сценарий может снова получить событие. Используйте его для приветствия, но не полагайтесь на то, что оно придёт ровно один раз без дублей от ретраев вебхука.
Эхо-бот с нуля (Python + Flask)
Идея:message_created → извлечь текст и адресата для POST /messages: в личке в query обычно нужен user_id отправителя, в чате/канале — chat_id (ориентир — recipient.chat_type: dialog / chat / channel) → быстро ответить 200 на вебхук. Ниже — актуальный пример из репозитория bothost_webhook-max (echo_bot.py): логирование при сбое, идемпотентность по mid, проверка секрета только если задан WEBHOOK_SECRET, HEAD на /webhook.
Установка: pip install flask requests.
Переменные окружения:
MAX_BOT_TOKEN— токен из кабинета.WEBHOOK_SECRET— тот жеsecret, что вPOST /subscriptions, если подписка с секретом; если переменная не задана, заголовокX-Max-Bot-Api-Secretне проверяется (удобно для отладки, в проде задайте пару «подписка ↔ env»).- Опционально
MAX_USE_BEARER=1— добавить префиксBearerк токену для исходящих запросов, если без негоGET /meили/messagesдают 401. PORT— порт HTTP (на Bothost задаёт платформа; локально по умолчанию 8080).
import os
import json
import hmac
import logging
import requests
from flask import Flask, request, jsonify
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("echo")
app = Flask(__name__)
MAX_API = "https://platform-api.max.ru"
WEBHOOK_SECRET = os.environ.get("WEBHOOK_SECRET", "")
TOKEN = (os.environ.get("MAX_BOT_TOKEN") or "").strip()
USE_BEARER = os.environ.get("MAX_USE_BEARER", "").lower() in ("1", "true", "yes")
# Идемпотентность: последние обработанные ключи (в памяти; в проде — Redis/БД)
_SEEN_MID = set()
_SEEN_MAX = 500
def auth_value() -> str:
if USE_BEARER and not TOKEN.lower().startswith("bearer "):
return f"Bearer {TOKEN}"
return TOKEN
def api_headers():
return {
"Authorization": auth_value(),
"Content-Type": "application/json",
}
def extract_message_payload(data: dict):
"""Возвращает (user_id, chat_id, chat_type, text, mid) из Update или пять None.
Для POST /messages MAX: в личке (dialog) в query обычно нужен user_id отправителя;
в группе/канале — chat_id. См. https://dev.max.ru/docs-api/methods/POST/messages
и recipient.chat_type в Update.
"""
if data.get("update_type") != "message_created":
return None, None, None, None, None
msg = data.get("message")
if not isinstance(msg, dict):
logger.warning("message_created без объекта message: keys=%s", list(data.keys()))
return None, None, None, None, None
recipient = msg.get("recipient")
if not isinstance(recipient, dict):
logger.warning("Нет recipient; message keys=%s", list(msg.keys()))
return None, None, None, None, None
body = msg.get("body")
if not isinstance(body, dict):
logger.warning("Нет body; message=%s", json.dumps(msg, ensure_ascii=False)[:800])
return None, None, None, None, None
chat_id = recipient.get("chat_id")
chat_type = recipient.get("chat_type")
text = (body.get("text") or "").strip()
mid = body.get("mid")
user_id = None
sender = msg.get("sender")
if isinstance(sender, dict) and not sender.get("is_bot"):
user_id = sender.get("user_id")
return user_id, chat_id, chat_type, text, mid
def send_max_message(
user_id: int | None,
chat_id: int | None,
recipient_chat_type: str | None,
text: str,
) -> None:
"""POST /messages: user_id или chat_id в query (официальный пример MAX)."""
url = f"{MAX_API}/messages"
params = {}
ct = (recipient_chat_type or "").strip().lower()
if ct in ("chat", "channel") and chat_id is not None:
params["chat_id"] = int(chat_id)
elif user_id is not None:
params["user_id"] = int(user_id)
elif chat_id is not None:
params["chat_id"] = int(chat_id)
else:
logger.warning("send_max_message: нет user_id и chat_id, ответ не отправлен")
return
body = {"text": text}
try:
r = requests.post(url, headers=api_headers(), params=params, json=body, timeout=15)
if not r.ok:
logger.error("messages API: %s %s", r.status_code, r.text[:500])
except requests.RequestException as e:
logger.exception("Ошибка сети при POST /messages: %s", e)
def remember_mid(mid) -> bool:
"""True если уже обрабатывали (дубликат)."""
if not mid:
return False
if mid in _SEEN_MID:
return True
if len(_SEEN_MID) >= _SEEN_MAX:
_SEEN_MID.clear()
_SEEN_MID.add(mid)
return False
@app.route("/webhook", methods=["POST", "HEAD"])
def webhook():
if request.method == "HEAD":
return "", 200
if WEBHOOK_SECRET:
got = request.headers.get("X-Max-Bot-Api-Secret", "")
if not hmac.compare_digest(got, WEBHOOK_SECRET):
return jsonify({"error": "forbidden"}), 403
else:
logger.warning(
"WEBHOOK_SECRET не задан — вебхук без проверки заголовка (только для отладки, в проде задайте secret)"
)
data = request.get_json(silent=True)
if not isinstance(data, dict):
logger.warning("Тело не JSON или пусто")
return jsonify({"ok": True}), 200
update_type = data.get("update_type")
if update_type == "message_created":
user_id, chat_id, chat_type, user_text, mid = extract_message_payload(data)
if mid and remember_mid(mid):
logger.info("Пропуск дубликата по mid=%s", mid)
return jsonify({"ok": True}), 200
if user_text and (user_id is not None or chat_id is not None):
send_max_message(
int(user_id) if user_id is not None else None,
int(chat_id) if chat_id is not None else None,
str(chat_type) if chat_type is not None else None,
f"Эхо: {user_text}",
)
elif user_id is None and chat_id is None:
logger.warning(
"Не удалось извлечь user_id/chat_id; payload=%s",
json.dumps(data, ensure_ascii=False)[:1200],
)
elif update_type == "bot_started":
logger.info("bot_started: %s", json.dumps(data, ensure_ascii=False)[:500])
return jsonify({"ok": True}), 200
@app.route("/health", methods=["GET"])
def health():
return jsonify({"status": "ok"}), 200
if __name__ == "__main__":
if not TOKEN:
raise SystemExit("Задайте MAX_BOT_TOKEN")
port = int(os.environ.get("PORT", "8080"))
app.run(host="0.0.0.0", port=port)Update — только по официальной схеме. При сбое включите временно logger.setLevel(logging.DEBUG) или один раз залогируйте json.dumps(data, indent=2, ensure_ascii=False) (не в проде на все запросы — ПДн и шум).
Запуск, HTTPS и подписка
Запуск:python echo_bot.py. Снаружи — HTTPS (nginx + Let's Encrypt или туннель вроде ngrok для теста).
Подписка (тот же secret, что в WEBHOOK_SECRET):
curl -X POST "https://platform-api.max.ru/subscriptions" \
-H "Authorization: ВАШ_MAX_BOT_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"url\":\"https://ваш-домен/webhook\",\"update_types\":[\"message_created\",\"bot_started\"],\"secret\":\"my-secret-12345\"}"Если что-то не работает
- 401 на
/meили/messages— токен, форматAuthorization(попробуйтеMAX_USE_BEARER=1или наоборот уберите Bearer, если копировали с префиксом). - 403 на вебхуке —
secretв подписке иWEBHOOK_SECRETв коде не совпадают, либо в подписке безsecret, а в окружении заданWEBHOOK_SECRET(MAX не пришлёт заголовок — проверка не пройдёт). Если секрет в подписке не используете, уберитеWEBHOOK_SECRETиз env или задайте пару «подписка ↔ env». - Нет эхо — логи
Не удалось извлечь user_id/chat_id+ сверка JSON с Update. Unknown recipientнаPOST /messages— неверный получатель: для лички используйтеuser_idв query, для группы/канала —chat_id(см. актуальный пример в bothost_webhook-max).- Дубли эхо — ретраи вебхука; расширьте идемпотентность (Redis по
mid, комбинация полей из документации).
Быстрый ответ 200 и очередь
В продакшене: проверка секрета → немедленно 200 → событие в очередь (Redis, RQ, Celery) → воркеры зовут/messages. Тяжёлая логика не должна блокировать ответ до таймаута 30 с.
Чек-лист для продакшен-бота
- [ ]
Authorizationпроверен тестомGET /me. - [ ] Подписка с
secret— в кодеhmac.compare_digestдляX-Max-Bot-Api-Secret(еслиWEBHOOK_SECRETне задан, проверку заголовка в примере из bothost_webhook-max можно не выполнять — только для отладки). - [ ] HEAD/GET на пути вебхука при необходимости для прогрева балансировщика.
- [ ] Идемпотентность по стабильным полям из Update (например
mid). - [ ] Обработка 429 и
Retry-Afterна исходящих вызовах. - [ ] Логи без токенов и секретов; при ошибках — алерты.
Отладка без своего домена
Для просмотра JSON удобны long polling или туннель к localhost. Для постоянной работы с MAX извне — вебхук с нормальным HTTPS.Хостинг бота на Bothost
На Bothost задаютWEBHOOK_URL, MAX_TOKEN / MAX_BOT_TOKEN и т.д. — ваш процесс отвечает 200, исходящие запросы идут на platform-api.max.ru с тем же токеном. Готовый эхо-пример с README под эту схему — репозиторий bothost-tech/bothost_webhook-max.
Куда смотреть дальше
POST /subscriptions, События, POST /messages, Update.Ещё раз: при расхождении текста статьи с dev.max.ru верьте документации MAX.
386 просмотров
0 лайков
0 комментариев
Комментарии (0)
Пока нет комментариев. Будьте первым!