SMeetBot API

SMeet Bot API

Сообщения и кнопки

Бот общается обычным текстом с кнопками под сообщениями. Здесь описано, что приходит боту, что он может отправить и какие правила не дают старым или скопированным кнопкам сработать не там.

Получение сообщений#

Сообщение человека приходит событием типа message:

JSON
{
  "update_id": "43",
  "event_id": "c2a1f5d0-0b7e-4a52-9f11-5a8c3e2d7b64",
  "type": "message",
  "date": "2026-09-26T10:00:04Z",
  "message": {
    "message_id": "7302",
    "chat": {"id": "550", "type": "private"},
    "from": {"id": "812", "display_name": "Anna", "username": "anna", "is_bot": false},
    "date": "2026-09-26T10:00:04Z",
    "text": "Where is my order A-1001?",
    "reply_to_message_id": "7300"
  }
}
  • chat.id передаётся как chat_id, когда вы отвечаете. Каждый чат личный: один человек и ваш бот.
  • from содержит отображаемое имя человека и, если он есть, публичный адрес. E-mail, телефон, другие чаты и сессии не передаются никогда.
  • text содержит читаемый текст. reply_to_message_id есть, если человек ответил на сообщение.
  • attachments перечисляет фото и документы (Фото и документы).
  • has_unsupported_content: true означает, что в сообщении есть то, что версия 1 не передаёт: голосовое или видеосообщение, геопозиция, опрос или чек-лист, а также файлы, если работа с файлами выключена. Тогда text содержит ту читаемую часть, которая есть.

Бот никогда не получает свои собственные сообщения, сообщения других ботов и системные уведомления, поэтому два бота не могут бесконечно отвечать друг другу. Сообщение, которое человек переслал боту, приходит обычным текстом, без сведений об исходном чате и авторе.

Команды#

Командой считается сообщение, текст которого начинается с /, например /help или /track A-1001. Боту она приходит обычным message; что она означает, решает ваша программа. Возможна и форма /help@support_helper_bot, поэтому берите часть до @.

Меню команд, которое люди видят после ввода «/», задаётся в BotFather или через setMyCommands (Управление ботом). Это только подсказка: человек может ввести любую команду.

/stop обрабатывает сам SMeet: чат останавливается (Организации и доступ), сообщение остаётся в истории, а бот получает только chat_access_changed со stopped, но не само сообщение.

Ссылка на бота может нести параметр: https://messenger.scrile.com/u/support_helper_bot?start=A-1001. Параметр содержит от 0 до 64 символов из A-Z a-z 0-9 _ -. Когда человек открывает ссылку и нажимает Начать, бот получает два события:

  1. chat_access_changed с "status": "started" и "start_param": "A-1001";
  2. message человека с текстом /start A-1001 (просто /start, если параметра нет).

Реагируйте на одно из двух, а не на оба. Большинство ботов используют сообщение, чтобы ссылка и набранная вручную команда /start A-1001 работали одинаково. Нажатие «Начать» в уже запущенном чате даёт только сообщение. Человек, который остановил бота, может и просто написать в чат /start, чтобы запустить его снова.

Отправка сообщения#

Shell
curl -s https://messenger.scrile.com/bot-api/v1/sendMessage \
  -H "Authorization: Bearer $SMEET_BOT_TOKEN" \
  -H "Idempotency-Key: c2a1f5d0-0b7e-4a52-9f11-5a8c3e2d7b64:reply" \
  -H "Content-Type: application/json" \
  -d '{"chat_id": "550", "text": "Your order A-1001 ships tomorrow.", "reply_to_message_id": "7302"}'
JSON
{
  "ok": true,
  "result": {
    "message_id": "7303",
    "chat": {"id": "550", "type": "private"},
    "from": {"id": "5001", "display_name": "Support helper", "username": "support_helper_bot", "is_bot": true},
    "date": "2026-09-26T10:00:05Z",
    "text": "Your order A-1001 ships tomorrow.",
    "reply_to_message_id": "7302"
  }
}
  • Отправитель всегда бот, которому принадлежит токен; параметра, чтобы писать от чужого имени, нет.
  • Текст передаётся обычным текстом, от 1 до 4096 символов. Он не может начинаться с __META__: или __SYSTEM__, а управляющие символы, кроме перевода строки и табуляции, отклоняются.
  • Idempotency-Key обязателен. Стройте его из события, на которое отвечаете, как в примере, и повторно доставленное событие никогда не даст второго сообщения (Idempotency-Key).
ОшибкаКогда
404 CHAT_NOT_FOUNDЧат не принадлежит этому боту. Чаты других ботов выглядят так же, как несуществующие.
403 BOT_STOPPED_OR_BLOCKEDЧеловек остановил или заблокировал бота либо вышел из его пространства. Больше не пишите в этот чат.
404 MESSAGE_NOT_FOUNDreply_to_message_id указывает не на сообщение этого чата.
400 INVALID_REQUESTНеверный текст или кнопки; error.field называет параметр.
429 RATE_LIMITEDСлишком быстро; подождите retry_after и повторите с тем же ключом.

Ответы на сообщения#

Передайте reply_to_message_id с сообщением из того же чата, и человек увидит ваше сообщение как ответ с цитатой исходного. Цитату строит сам SMeet из первых 200 символов исходного текста. Цитата учитывается в 4096 символах, поэтому очень длинный ответ может получить INVALID_REQUEST с "field": "text".

Кнопки#

Кнопки размещаются под сообщением в reply_markup.inline_keyboard рядами:

JSON
{
  "chat_id": "550",
  "text": "Выберите вопрос:",
  "reply_markup": {
    "inline_keyboard": [
      [{"text": "Часы работы", "callback_data": "faq:hours"}, {"text": "Доставка", "callback_data": "faq:delivery"}],
      [{"text": "О ботах SMeet", "url": "https://messenger.scrile.com/docs/bots/ru/"}]
    ]
  }
}

У каждой кнопки есть text и ровно одно из полей:

  • callback_data, от 1 до 64 байт: нажатие приходит боту как callback_query с этими данными. Приложение человека эти данные не видит никогда; SMeet хранит их на сервере и даёт каждой кнопке собственный идентификатор.
  • url, ссылка https:// без имени пользователя и пароля, до 2048 символов: приложение открывает её, а бот об этом не узнаёт.

До 10 рядов, до 8 кнопок в ряду и до 40 кнопок всего; текст кнопки от 1 до 64 символов и не из одних пробелов. Кнопки работают в веб-приложении и в SMeet для iOS и macOS.

В приложениях без поддержки ботов#

Приложения без поддержки ботов (SMeet для Android до выпуска с ботами, а также старые версии веб-приложения и SMeet для iOS) не дают нажать Начать. В уже запущенном чате они показывают сообщения бота обычным текстом без кнопок, а человек по-прежнему может отвечать текстом и набирать команды. Поэтому дублируйте каждое важное действие кнопки командой из меню, например /status рядом с кнопкой Статус, и упоминайте её в тексте: тогда чат остаётся пригодным и в этих приложениях.

Нажатия кнопок#

Нажатие callback-кнопки приходит как callback_query:

JSON
{
  "update_id": "45",
  "event_id": "5b9e0c3a-2d41-4f7a-8e6b-1c2d3e4f5a6b",
  "type": "callback_query",
  "date": "2026-09-26T10:00:09Z",
  "callback_query": {
    "id": "cb_EXAMPLE",
    "from": {"id": "812", "display_name": "Anna", "is_bot": false},
    "message": {"message_id": "7304", "chat": {"id": "550", "type": "private"}},
    "data": "faq:hours",
    "date": "2026-09-26T10:00:09Z"
  }
}

Ответьте на него через answerCallbackQuery, затем выполните работу, например измените сообщение:

  • Приложение человека показывает индикатор ожидания. Если ответа нет 15 секунд, оно показывает Бот не ответил, попробуйте позже; более поздний ответ всё равно применится, пока чат открыт.
  • На нажатие можно ответить один раз и в течение 1 часа. Повтор того же ответа безвреден; другой второй ответ получает 409 IDEMPOTENCY_CONFLICT; после часа приходит 404 CALLBACK_QUERY_NOT_FOUND.
  • text до 200 символов показывается человеку коротким уведомлением; "show_alert": true показывает его диалогом.

Так нажатие обрабатывает python/faq_bot.py:

Pythondocs/bots/examples/python/faq_bot.py
def handle(client: smeet.Client, update: dict) -> None:
    event_id = update["event_id"]
    if update["type"] == "message":
        chat_id = update["message"]["chat"]["id"]
        client.send_message(chat_id, MENU_TEXT, reply_markup=menu_markup(),
                            idempotency_key=smeet.action_key(event_id, "menu"))
        log.info("Menu sent to chat %s", chat_id)
        return
    if update["type"] != "callback_query":
        return  # access changes, edits and unknown types need no answer here

    query = update["callback_query"]
    chat_id = query["message"]["chat"]["id"]
    message_id = query["message"]["message_id"]
    data = query["data"]
    if data == "menu":
        text, markup = MENU_TEXT, menu_markup()
    elif data.startswith("faq:") and data[len("faq:"):] in FAQ:
        title, answer = FAQ[data[len("faq:"):]]
        text, markup = f"{title}\n\n{answer}", BACK_MARKUP
    else:
        # A button from an older version of this bot: say so instead of leaving the user waiting.
        client.answer_callback_query(query["id"], text="This button is out of date. Send /start.",
                                     idempotency_key=smeet.action_key(event_id, "answer"))
        return

    # Answer the press first: it stops the waiting indicator in the app. Then edit the message.
    client.answer_callback_query(query["id"], idempotency_key=smeet.action_key(event_id, "answer"))
    client.edit_message_text(chat_id, message_id, text, reply_markup=markup,
                             idempotency_key=smeet.action_key(event_id, "edit"))
    log.info("Button %s handled in chat %s", data, chat_id)

Редактирование сообщений#

editMessageText меняет текст и кнопки сообщения, которое этот бот отправил в этот чат; для любого другого сообщения приходит 404 MESSAGE_NOT_FOUND.

  • text обязателен и заменяет текст. Ответ сохраняет свою цитату.
  • reply_markup заменяет кнопки. Без него после правки у сообщения не будет кнопок.
  • Новые кнопки заменяют старые целиком. Нажатие на старые кнопки, например на втором устройстве, которое ещё не обновилось, отклоняется, и человек видит, что кнопка больше не действует.
  • Приложения людей сразу обновляют сообщение. В результате есть edit_date. О своей собственной правке бот события не получает.
  • Чат должен оставаться запущенным: после остановки бот не может и редактировать свои сообщения.

Изменённые и удалённые сообщения#

  • Когда человек изменяет сообщение, бот получает message_edited с полным новым текстом в edited_message и edit_date. Это исправление, а не новая команда: не выполняйте команду повторно из-за того, что её сообщение изменили.
  • Когда человек удаляет сообщение, которое бот ещё не получил, его событие отменяется и не доставляется. Если бот его уже получил, приходит событие message_deleted с message_id; текст не повторяется.
  • Файлы удалённого сообщения сразу становятся недоступны боту.

Старые, пересланные и отложенные сообщения#

  • Ничего из того, что человек отправил до нажатия «Начать», до бота не доходит. После остановки и нового запуска события, отменённые остановкой, не возвращаются.
  • Нажатие работает только на текущих кнопках сообщения, которое ещё существует, в запущенном чате. Старые версии кнопок, удалённые сообщения и чаты после остановки нажатия отклоняют.
  • У пересланной копии сообщения бота нет кнопок: пересылка не может перенести действие в другой чат.
  • Сообщение, которое человек запланировал на потом, становится событием только в момент фактической отправки. Если его удалят раньше, бот о нём не узнает.

Уведомления только с согласия#

Нажатие «Начать» разрешает разговор, а не поток сообщений. Бот, который присылает уведомления (статус заказа, напоминание), должен явно спросить согласие, например кнопкой Уведомлять меня, и сохранять подписку только после этого нажатия. Весь сценарий показан в python/status_bot.py.

Получив chat_access_changed со stopped, blocked или removed, бот должен удалить подписки этого чата. Если уведомление разминулось с остановкой, о которой бот ещё не узнал, SMeet отвечает 403 BOT_STOPPED_OR_BLOCKED, и бот должен поступить так же:

Pythondocs/bots/examples/python/status_bot.py
def set_chat_active(db: sqlite3.Connection, chat_id: str, active: bool) -> None:
    """Mark a chat as reachable or not. A chat that is not reachable loses all its subscriptions."""
    with db:  # one transaction
        db.execute("INSERT OR REPLACE INTO chats (chat_id, active, updated_at) VALUES (?, ?, ?)",
                   (chat_id, 1 if active else 0, time.time()))
        if not active:
            db.execute("DELETE FROM subscriptions WHERE chat_id = ?", (chat_id,))

Ключ Idempotency-Key уведомления стройте из того, о чём оно, тогда повторный запуск того же уведомления ничего нового не отправит:

Pythondocs/bots/examples/python/status_bot.py
def notification_key(application_id: str, status: str, chat_id: str, change_id: Optional[str]) -> str:
    raw = "\n".join((application_id, status, change_id or "", chat_id))
    return "notify-" + hashlib.sha256(raw.encode("utf-8")).hexdigest()[:40]

Ограничения#

ЧтоОграничение
Текст сообщенияОт 1 до 4096 символов; не начинается с __META__: или __SYSTEM__; без управляющих символов, кроме перевода строки и табуляции
Подпись к фото или документуДо 1024 символов, те же правила
Цитата в ответеДо 200 символов исходного текста, входит в 4096
КнопкиДо 10 рядов, до 8 в ряду, до 40 всего
Текст кнопкиОт 1 до 64 символов, не из одних пробелов
callback_dataОт 1 до 64 байт в UTF-8
url кнопкиТолько https://, до 2048 символов, без имени пользователя и пароля
Ответ на нажатиеОдин раз, в течение 1 часа; уведомление до 200 символов
Меню командДо 100 команд; команда от 1 до 32 символов из a-z 0-9 _
Темп отправки1 сообщение в секунду в один чат (всплеск 3), 10 в секунду на бота (всплеск 20), 20 в секунду на всех ботов одного владельца (всплеск 40)