Publisher API
Publisher API — это REST-интерфейс MotivBuy, через который ваш бот в МАКС запрашивает у платформы набор обязательных подписок для конкретного пользователя и сообщает о выполнении задания. Все запросы — JSON, аутентификация через Bearer-токен, выданный для каждого подключённого бота.
Аутентификация
Каждый запрос должен содержать заголовок Authorization: Bearer <api_key>. API-ключ привязан к одному боту: токен, использованный для чужого бота, вернёт ошибку 403 forbidden_error.
Получить ключ можно в личном кабинете MotivBuy: откройте карточку бота → раздел Интеграция → кнопка Скопировать API-ключ. Там же доступна ротация ключа — после неё старое значение мгновенно перестаёт работать.
Никогда не публикуйте API-ключ
Базовый URL
Все эндпоинты доступны по адресу https://api.motivbuy.ru. Прод-хост может быть заменён вашим выделенным доменом — в этом случае значение придёт вместе с приглашением в программу.
Поток интеграции
Типовой сценарий встраивания MotivBuy в пользовательский путь занимает два сетевых вызова:
- Перед целевым действием (скачивание, поиск, выдача премиум-контента — не на каждый
/start) вызовитеPOST /publisher/requestс идентификаторами пользователя и чата. - Если в ответе
all_subscribed: true— обязательные подписки уже выполнены, выполняйте целевое действие. - Иначе MotivBuy сам отправит пользователю OP-сообщение через токен вашего бота — со списком спонсоров и кнопкой проверки
op:check. - Когда пользователь нажимает
op:check, ваш бот вызываетPOST /publisher/verify. Еслиall_subscribed: true— задание выполнено, переходите к основному контенту.
POST /publisher/request
Запрашивает у MotivBuy набор обязательных подписок для конкретного пользователя. Вызывается на старте диалога и перед показом любого чувствительного контента.
Параметры тела запроса
| Поле | Тип | Обязательное | Описание |
|---|---|---|---|
max_user_id | integer | да | ID пользователя МАКС (берётся из любого события: BotStarted, MessageCreated.sender или MessageCallback.user). |
chat_id | integer | да | ID диалога с этим пользователем, куда отправлять OP-сообщение. |
country_code | string (2) | нет | ISO-код страны для демографического таргетинга, например "RU". |
language_code | string (≤8) | нет | Язык интерфейса пользователя, например "ru". |
age | integer (0..120) | нет | Возраст пользователя, если он известен. |
gender | "MALE" | "FEMALE" | нет | Пол пользователя. |
is_premium | boolean | нет | Признак премиум-пользователя. |
op_text | string (≤1024) | нет | Текст OP-сообщения, переопределяет шаблон бота для этого вызова. |
check_button | string (≤64) | нет | Текст кнопки проверки подписок для этого вызова. |
Примеры запроса
curl -X POST https://api.motivbuy.ru/api/v1/publisher/request \
-H "Authorization: Bearer sk_live_xxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"max_user_id": 123456789,
"chat_id": 987654321,
"country_code": "RU",
"language_code": "ru",
"age": 27,
"gender": "MALE",
"is_premium": false
}'import aiohttp
API_KEY = "sk_live_xxxxxxxxxxxxxxxxxxxxxxxx"
BASE_URL = "https://api.motivbuy.ru"
async def request_subscriptions(max_user_id: int, chat_id: int) -> dict:
headers = {"Authorization": f"Bearer {API_KEY}"}
payload = {
"max_user_id": max_user_id,
"chat_id": chat_id,
"country_code": "RU",
"language_code": "ru",
"is_premium": False,
}
async with aiohttp.ClientSession(headers=headers) as session:
async with session.post(
f"{BASE_URL}/api/v1/publisher/request",
json=payload,
) as response:
response.raise_for_status()
return await response.json()const API_KEY = "sk_live_xxxxxxxxxxxxxxxxxxxxxxxx";
const BASE_URL = "https://api.motivbuy.ru";
async function requestSubscriptions(maxUserId, chatId) {
const response = await fetch(`${BASE_URL}/api/v1/publisher/request`, {
method: "POST",
headers: {
Authorization: `Bearer ${API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
max_user_id: maxUserId,
chat_id: chatId,
country_code: "RU",
language_code: "ru",
is_premium: false,
}),
});
if (!response.ok) {
throw new Error(`MotivBuy /request failed: ${response.status}`);
}
return response.json();
}Ответ 200 OK
{
"all_subscribed": false,
"channels_left": 3,
"sent_message_id": "mid.12345"
}all_subscribed: true— пользователь уже состоит во всех обязательных каналах. OP-сообщение не отправлено, старое (если было) удалено автоматически. Продолжайте основной сценарий/start.all_subscribed: false— MotivBuy отправил OP-сообщение в указанныйchat_idчерез токен вашего бота. Вsent_message_idвозвращается ID отправленного сообщения, вchannels_left— сколько ещё подписок осталось выполнить.
POST /publisher/verify
Проверяет, выполнил ли пользователь обязательные подписки. Вызывается, когда пользователь нажимает кнопку op:check под OP-сообщением.
Параметры тела запроса
| Поле | Тип | Обязательное | Описание |
|---|---|---|---|
max_user_id | integer | да | ID пользователя МАКС. |
chat_id | integer | да | ID диалога с пользователем. |
op_text | string (≤1024) | нет | Переопределение текста OP-сообщения, если оно будет переотправлено. |
check_button | string (≤64) | нет | Переопределение текста кнопки проверки. |
Примеры запроса
curl -X POST https://api.motivbuy.ru/api/v1/publisher/verify \
-H "Authorization: Bearer sk_live_xxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"max_user_id": 123456789,
"chat_id": 987654321
}'import aiohttp
API_KEY = "sk_live_xxxxxxxxxxxxxxxxxxxxxxxx"
BASE_URL = "https://api.motivbuy.ru"
async def verify_subscriptions(max_user_id: int, chat_id: int) -> dict:
headers = {"Authorization": f"Bearer {API_KEY}"}
payload = {"max_user_id": max_user_id, "chat_id": chat_id}
async with aiohttp.ClientSession(headers=headers) as session:
async with session.post(
f"{BASE_URL}/api/v1/publisher/verify",
json=payload,
) as response:
response.raise_for_status()
return await response.json()const API_KEY = "sk_live_xxxxxxxxxxxxxxxxxxxxxxxx";
const BASE_URL = "https://api.motivbuy.ru";
async function verifySubscriptions(maxUserId, chatId) {
const response = await fetch(`${BASE_URL}/api/v1/publisher/verify`, {
method: "POST",
headers: {
Authorization: `Bearer ${API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
max_user_id: maxUserId,
chat_id: chatId,
}),
});
if (!response.ok) {
throw new Error(`MotivBuy /verify failed: ${response.status}`);
}
return response.json();
}Ответ 200 OK
{
"all_subscribed": true,
"channels_left": 0,
"sent_message_id": null
}Структура ответа идентична /publisher/request. Если all_subscribed: true — задание выполнено, открывайте пользователю основной контент. Иначе — MotivBuy обновит OP-сообщение со списком оставшихся подписок, пользователю нужно нажать кнопку проверки ещё раз.
Ошибки
Все ошибки возвращаются в формате { "code": "...", "message": "..." } с соответствующим HTTP-статусом.
| Статус | code | Причина |
|---|---|---|
401 | auth_error | Отсутствует или некорректен заголовок Authorization. |
403 | forbidden_error | Бот не одобрен модерацией или ключ принадлежит другому боту. |
404 | not_found | Ресурс не найден (например, пользователь или чат не существует). |
422 | validation_error | Невалидное тело запроса: не прошли проверки типов, длины или диапазонов. |
429 | rate_limit_error | Превышен лимит запросов: глобальный по ключу или по паре (ключ, пользователь). |
Лимиты
Запросы ограничиваются на двух уровнях: глобально по API-ключу и отдельно по паре (api_key, max_user_id) — не чаще одного запроса в секунду на конкретного пользователя. При превышении возвращается 429 rate_limit_error; повторяйте запрос с экспоненциальной задержкой.
Best practices
- Идемпотентность. Повторный вызов
/requestс тем жеmax_user_idв течение срока сброса ссылки возвращает тот же набор спонсоров — можно безопасно повторять запросы при перезапуске диалога. - Валидация чата. Всегда передавайте
chat_idровно тот, что пришёл в событииBotStarted: платформа проверяет, что чат принадлежит боту, выпустившему ключ. - Кеш на 30 секунд. Кешируйте последний ответ
/requestдля пары(api_key, max_user_id)на 30 секунд — это спасёт от двойных кликов по стартовой кнопке и лишних 429. - Не дёргайте
/verifyдо клика. Эндпоинт рассчитан на ответ на callbackop:check. Пуш-опросы по таймеру быстро упрутся в rate-limit. - Логируйте
sent_message_id. Это пригодится при разборе инцидентов и для серверной аналитики доставки OP-сообщений.