Shalter Bot API
Документация для разработчиков
Открыть Shalter

Bot API

Бот в Shalter — обычный аккаунт, которым управляет программа. Он состоит в чатах, пишет сообщения, показывает кнопки. Программу держите где угодно и пишите на чём угодно — через Bot API с токеном: встроенного редактора кода больше нет, бот работает с вашего сервера.

Все методы

53 методов. Все — по адресу /api/bot-api, все с заголовком Authorization: Bearer …, все принимают и отдают JSON.

Сообщения
POST /sendMessage
POST /sendPhoto
POST /sendDocument
POST /sendVideo
POST /sendVoice
POST /sendVideoNote
POST /sendSticker
POST /sendPoll
POST /sendLocation
POST /sendContact
POST /forwardMessage
POST /editMessageText
POST /editMessageKeyboard
POST /deleteMessage
POST /sendReaction
GET /getMessage
GET /getMessages
GET /searchMessages
GET /getPinnedMessages
POST /pinChatMessage
POST /unpinChatMessage
POST /sendChatAction
Отложенная отправка
POST /scheduleMessage
GET /getScheduled
POST /cancelScheduled
Чаты
GET /getChats
GET /getChat
POST /createGroup
POST /setChatTitle
POST /setChatDescription
POST /setChatPermissions
POST /exportChatInviteLink
POST /leaveChat
Участники
GET /getChatAdmins
GET /getChatMember
GET /getChatMemberCount
POST /addChatMember
POST /promoteChatMember
POST /restrictChatMember
POST /banChatMember
POST /deleteAnyMessage
POST /sendGame
POST /getChatJoinRequests
POST /approveChatJoinRequest
POST /declineChatJoinRequest
Люди
GET /resolveUsername
GET /getUser
GET /getUserStatus
GET /getCommonChats
Каналы
POST /publishPost
GET /getChannelStats
Бот
GET /me
GET /updates
GET /getMyCommands
POST /setMyCommands
POST /setMyProfile
GET /getMyStats

Два способа сделать бота

Свой сервер, свой язык

Скрипт на вашей машине спрашивает у Shalter новые сообщения и отвечает на них. Нужен только исходящий интернет — ни белого IP, ни домена, ни вебхуков. Любой язык, любые библиотеки, своя база данных. Это и есть HTTP API ниже.

Редактор внутри Shalter

Функция на JavaScript, которая исполняется на сервере при каждом входящем сообщении. Ни токена, ни хостинга, ни цикла опроса. Быстрее всего для небольшого бота — и заметно ограниченнее, о чём честно сказано в разделе про ограничения.

Токен

Настройки → Боты → «Создать бота». У бота сразу появляется собственный аккаунт с юзернеймом вида @name_bot и токен — длинная строка, которая и есть право говорить от его имени.

Потерять токен нестрашно: кнопка с ключом рядом с ботом показывает действующий в любой момент. Перевыпуск (кнопка обновления) выдаёт новый и делает старый недействительным — работающая программа со старым токеном сразу перестанет отвечать, так что перевыпускайте только если он утёк.

Токен — это пароль бота. Кто им владеет, тот и есть бот: читает его чаты и пишет от его имени. Не кладите его в публичный репозиторий и не вставляйте в код фронтенда — только в переменные окружения на своей стороне.

Быстрый старт

Проверить, что токен рабочий, — одна команда:

curl -H "Authorization: Bearer ВАШ_ТОКЕН" \
  https://shalter.ru/api/bot-api/me

В ответ придёт аккаунт бота. Дальше — рабочий бот целиком, на голом Node без единой зависимости:

const TOKEN = process.env.SHALTER_TOKEN;
const API = "https://shalter.ru/api/bot-api";
const auth = { Authorization: `Bearer ${TOKEN}`, "Content-Type": "application/json" };

let after = new Date().toISOString(); // начинаем с «сейчас», старое не разгребаем

async function send(chatId, text) {
  await fetch(`${API}/sendMessage`, {
    method: "POST",
    headers: auth,
    body: JSON.stringify({ chatId, text }),
  });
}

async function tick() {
  const res = await fetch(`${API}/updates?after=${encodeURIComponent(after)}`, { headers: auth });
  const { messages } = await res.json();
  for (const msg of messages) {
    after = msg.createdAt; // следующий опрос — строго после последнего разобранного
    if (msg.text === "/start") await send(msg.chatId, "Привет! Я бот.");
    else await send(msg.chatId, `Вы написали: ${msg.text}`);
  }
}

setInterval(() => tick().catch(console.error), 2000);

Авторизация

Все запросы к /api/bot-api/* требуют заголовка:

Authorization: Bearer ВАШ_ТОКЕН

Без него — 401 с текстом Missing Authorization: Bearer <token>, с неверным — 401 Invalid bot token. Куки и сессии здесь не используются: бот не человек, он не «входит» в приложение.

Кто я

GET /api/bot-api/me

Возвращает аккаунт бота — имя, юзернейм, аватар. Нужен, чтобы проверить токен и узнать свой id.

{ "bot": { "id": "u_17…", "name": "Погодный", "username": "pogodnyy_bot", "isBot": true } }

Новые сообщения

GET /api/bot-api/updates?after=<ISO-время>

Отдаёт сообщения из чатов бота, пришедшие после указанного времени. Свои собственные сообщения бот в ответе не увидит — иначе он отвечал бы сам себе по кругу.

ПараметрТипОписание
afterстрока, ISO 8601Время последнего разобранного сообщения. Без него отдаётся всё с начала времён.
timeoutчисло, секунды (до 50)Длинный опрос: сервер не отвечает пустотой сразу, а держит запрос до первого сообщения. С ним бот отвечает мгновенно.
limitчисло (до 200)Сколько сообщений отдать за раз. По умолчанию 200.

Сообщения отсортированы по возрастанию времени, поэтому следующее значение after — это messages.at(-1).createdAt. За один запрос отдаётся не больше 200 сообщений: разговорчивый бот не должен получить в ответ всю свою историю.

Ставьте timeout — это и есть скорость бота. Без него задержка ответа равна паузе в вашем цикле: спите две секунды между запросами — нажатая кнопка молчит до двух секунд. С timeout=30 запрос висит до появления сообщения и возвращается в тот же миг, когда оно пришло, а пустой ответ через 30 секунд означает «просто спроси снова» — паузы между запросами не нужно вовсе.
let after = new Date().toISOString();
while (true) {
  const res = await fetch(`${API}/updates?after=${after}&timeout=30`, { headers: auth });
  const { messages } = await res.json();
  for (const m of messages) await handle(m);
  if (messages.length) after = messages.at(-1).createdAt;
}
Опрос, а не вебхук — намеренно. Вебхук требует публичного адреса и сертификата; опрос работает откуда угодно, включая ноутбук за NAT.

Отправка

POST /api/bot-api/sendMessage
ПолеТипОписание
chatIdстрокаОбязательное. Куда писать — берётся из msg.chatId входящего сообщения.
textстрокаОбязательное. Пустой текст отклоняется с 400.
replyToIdстрокаОтветить на конкретное сообщение — в чате будет видна цитата.
keyboardмассив рядовКнопки под сообщением, см. ниже.

Бот может писать только в те чаты, где он состоит: попытка написать в чужой чат вернёт 404 Bot is not a member of this chat. Это не формальность — иначе токен любого бота превращался бы в право рассылки по всему мессенджеру.

Бот пишет первым

sendMessage отправляет в чат, где бот уже состоит. Чтобы начать разговор самому — найти человека и написать ему, — есть отдельный метод. Ради него боты обычно и заводятся: напомнить о доставке, прислать код, сообщить о заказе.

POST /api/bot-api/sendMessageToUser
await fetch(`${API}/sendMessageToUser`, {
  method: "POST",
  headers: auth,
  body: JSON.stringify({
    username: "ivan",                  // или userId: "u_…"
    text: "Заказ №42 приедет к 18:00",
    keyboard: [[{ text: "Отследить", data: "/track 42" }]],
  }),
});
// → { chatId, message, user }
ПолеОписание
username@юзернейм получателя, с «@» или без.
userIdИдентификатор, если он у вас уже есть (например, из /updates).
textТекст сообщения.
keyboardКнопки — как в обычной отправке.
Поиска по номеру телефона здесь нет — и не будет. Метод, принимающий номер, перебором превращается в телефонную книгу: по нему выяснили бы, у кого есть аккаунт и как его зовут. @юзернейм — другое дело: человек сам решил его завести.

Отказы, которые надо уметь обрабатывать:

КодКогда
403Получатель запретил ботам писать первыми (Настройки → Конфиденциальность) или заблокировал этого бота.
404Такого @юзернейма нет.
429Больше 20 новых разговоров в час. Отвечать в уже открытых чатах можно без ограничений.
Почему ограничения именно такие. Без них этот метод — готовая рассылка спама: нашёл всех подряд по юзернеймам и написал каждому. Поэтому: человек может выключить такие сообщения одним переключателем, заблокировавшему бота не пишут вовсе, а число новых разговоров в час ограничено. Продолжать уже начатую переписку это никак не ограничивает.

Картинки и файлы

POST /api/bot-api/sendPhoto
POST /api/bot-api/sendDocument

Вложение передаётся ссылкой, а не загрузкой файла: у бота уже есть где хранить свои картинки, а второй путь загрузки на сервер тянул бы за собой свои лимиты, чистку и квоты. Годится и data:-URL, если файл маленький и генерируется на лету.

ПолеТипОписание
chatIdстрокаОбязательное.
urlстрокаОбязательное. Адрес картинки или файла.
captionстрокаПодпись — она же становится текстом сообщения.
nameстрокаТолько для sendDocument: имя файла, которое увидит человек.
replyToIdстрокаОтветить на сообщение.
await fetch(`${API}/sendPhoto`, {
  method: "POST", headers: auth,
  body: JSON.stringify({ chatId, url: "https://example.com/chart.png", caption: "Отчёт за неделю" }),
});

Правка и удаление

POST /api/bot-api/editMessageText
POST /api/bot-api/deleteMessage

Первый принимает messageId и новый text, второй — только messageId. Изменения видны всем в чате сразу, без перезагрузки.

Бот может править и удалять только свои сообщения — чужое вернёт 403. Это не формальность: токен бота живёт на чьём-то сервере, и право стирать чужие слова там ему не нужно.

const { message } = await (await fetch(`${API}/sendMessage`, {
  method: "POST", headers: auth, body: JSON.stringify({ chatId, text: "Считаю…" }),
})).json();

const result = await heavyWork();

await fetch(`${API}/editMessageText`, {
  method: "POST", headers: auth,
  body: JSON.stringify({ messageId: message.id, text: `Готово: ${result}` }),
});

Закрепление

POST /api/bot-api/pinChatMessage

Принимает messageId и pinned (true по умолчанию, false — открепить). Здесь правило другое, чем у правки: закрепить можно и чужое сообщение, но только там, где бот администратор. Закреп видит весь чат — это действие модератора, а не отправителя. В личном чате с ботом ограничения нет.

Печатает…

POST /api/bot-api/sendChatAction

Принимает chatId. Показывает в чате, что бот думает над ответом — то, что отличает занятого бота от зависшего. Живёт пять секунд, как и у людей: перед длинным шагом повторяйте, а не ставьте один раз в начале.

Необязательный action — что именно происходит, те же значения, что в Telegram Bot API: typing, choose_sticker, upload_photo, record_video, upload_video, record_voice, upload_voice, upload_document, record_video_note, upload_video_note. Без него — «печатает».

Чаты и люди

GET /api/bot-api/getChats

Все чаты, где состоит бот: id, тип, название, число участников и признак админа. Без него единственный способ узнать свой чат — дождаться, пока туда кто-нибудь напишет.

GET /api/bot-api/getChat?chatId=…

Один чат подробно, вместе со списком участников.

GET /api/bot-api/resolveUsername?username=@handle

Найти человека или канал по @юзернейму — то, чем бот ищет собеседника сам, а не ждёт, пока напишут ему. Возвращает kind (user, channel, group) и найденное.

Поиска по номеру телефона здесь нет и не будет. Метод, отвечающий «есть ли аккаунт у этого номера», — это готовая телефонная книга: перебором номеров он выдал бы, кто в мессенджере и как его зовут. @юзернейм — другое дело: он публичен по своей сути, человек сам решил его завести.
GET /api/bot-api/getUser?userId=…

Кто написал. В сообщении приходит только senderId; отсюда берутся имя и юзернейм, чтобы ответить «Спасибо, Аня», а не «Спасибо, u_1786…». Чужой адрес почты не отдаётся никогда — ни здесь, ни где-либо ещё.

История чата

GET /api/bot-api/getMessages?chatId=…&limit=60&before=…

Страница сообщений, свежие в конце. before — идентификатор сообщения, от которого идти назад; в ответе есть hasMore. За раз не больше 200. Читаются только чаты, где бот состоит.

Стикеры, опросы, реакции

POST /api/bot-api/sendSticker

chatId, emoji, необязательные name и scene (имя нарисованной сцены — см. набор «Shalter» в приложении).

POST /api/bot-api/sendPoll

chatId, question и options — от двух до восьми вариантов. Голосуют люди, бот только создаёт.

POST /api/bot-api/sendReaction

messageId и emoji. Повторный вызов с тем же эмодзи снимает реакцию — как и нажатие пальцем.

Права администратора

Всё в этом разделе требует, чтобы бота назначили администратором чата. Это то же правило, что и для человека: права даёт владелец чата, а не наличие токена.

POST /api/bot-api/deleteAnyMessage

Удалить чужое сообщение — то, ради чего заводят бота-модератора. Своё бот удаляет методом deleteMessage и без прав.

POST /api/bot-api/restrictChatMember

chatId, userId, minutes (до 30 дней). Временное молчание: человек остаётся в чате и видит переписку.

POST /api/bot-api/banChatMember

Убрать участника из чата.

POST /api/bot-api/getChatJoinRequests

chatId. Заявки на вступление в канал или группу с включёнными «Заявками на вступление»: userId, name, username, bio, createdAt.

POST /api/bot-api/approveChatJoinRequest
POST /api/bot-api/declineChatJoinRequest

chatId, userId. Принять или отклонить заявку — человек сразу увидит канал в списке или уведомление об отказе.

POST /api/bot-api/setChatTitle
POST /api/bot-api/setChatDescription
POST /api/bot-api/leaveChat

Уйти из чата — единственное здесь, что боту не нужно согласовывать.

Владельца и других администраторов бот не тронет. Попытка ограничить или выгнать их возвращает 400. Иначе одним утёкшим токеном можно было бы обезглавить чат.

Видео, голос, место, контакт

POST /api/bot-api/sendVideo
POST /api/bot-api/sendVoice
POST /api/bot-api/sendVideoNote

Те же поля, что у sendPhoto: chatId, url, caption, replyToId. Файл передаётся ссылкой — см. раздел «Картинки и файлы».

POST /api/bot-api/sendLocation

lat и lng — точка на карте.

POST /api/bot-api/sendContact

name, phone, необязательный userId.

Работа с сообщениями

POST /api/bot-api/forwardMessage

messageId и toChatId — оба чата должны быть чатами бота. В пересланном видно, откуда и от кого оно.

GET /api/bot-api/getMessage?messageId=…
GET /api/bot-api/searchMessages?chatId=…&q=…

Поиск внутри одного чата, до 50 последних совпадений.

GET /api/bot-api/getPinnedMessages?chatId=…
POST /api/bot-api/unpinChatMessage
POST /api/bot-api/editMessageKeyboard

Заменить кнопки под своим сообщением, не трогая текст — так бот обновляет клавиатуру после нажатия.

Отложенная отправка

POST /api/bot-api/scheduleMessage
GET /api/bot-api/getScheduled?chatId=…
POST /api/bot-api/cancelScheduled

sendAt — время в ISO 8601, обязательно будущее. Нужно затем же, зачем людям: разослать объявление в назначенный час, а не будить программу ночью.

Участники и права

POST /api/bot-api/createGroup

title и memberIds. Бот становится владельцем созданной группы.

Позвать можно только знакомых. В memberIds проходят лишь те, с кем бот уже состоит в общем чате — иначе токен превращался бы в право затащить любого человека в любую группу.
POST /api/bot-api/addChatMember
POST /api/bot-api/promoteChatMember

admin: true|false. Владельца изменить нельзя — он не «ещё один администратор».

POST /api/bot-api/setChatPermissions
POST /api/bot-api/exportChatInviteLink

revoke: true выпускает новую ссылку вместо старой.

GET /api/bot-api/getChatAdmins?chatId=…
GET /api/bot-api/getChatMember?chatId=…&userId=…

Отдаёт человека, его права и срок ограничения, если он есть.

GET /api/bot-api/getChatMemberCount?chatId=…
GET /api/bot-api/getUserStatus?userId=…
GET /api/bot-api/getCommonChats?userId=…

Общие чаты бота с этим человеком.

Каналы

POST /api/bot-api/publishPost
GET /api/bot-api/getChannelStats?chatId=…

Подписчики, посты, просмотры и комментарии. И то и другое — только для канала, где бот администратор.

Канал и его обсуждение

Связка «канал + группа комментариев» собирается скриптом целиком: завести канал, прицепить к нему обсуждение, опубликовать пост. Раньше это можно было сделать только руками через меню.

POST /api/bot-api/createChannel
const { chat } = await (await fetch(`${API}/createChannel`, {
  method: "POST",
  headers: auth,
  body: JSON.stringify({
    title: "Пиццерия · новости",
    description: "Акции и новинки",
    username: "pizza_news",   // необязательно
    isPublic: true,           // работает только вместе с username
  }),
})).json();

Владельцем канала становится не только бот, но и его хозяин: иначе канал, созданный программой, принадлежал бы программе — сменился токен, и живой человек остался бы без доступа к собственному каналу.

POST /api/bot-api/setChatDiscussion
actionЧто делает
createЗаводит новую группу-обсуждение и сразу цепляет её к каналу. Можно передать свой title.
linkЦепляет уже существующую группу — по groupId. Бот должен быть администратором и канала, и группы.
unlinkОтцепляет обсуждение. Сама группа остаётся: у неё свои участники и своя переписка.
// Канал → обсуждение → первый пост, одним скриптом
const ch = (await post("createChannel", { title: "Пиццерия · новости" })).chat;
const d  = (await post("setChatDiscussion", { channelId: ch.id, action: "create" })).discussion;
await post("publishPost", { chatId: ch.id, text: "Открылись! Первая пицца — в подарок." });
// комментарии к посту прилетят в d.id обычными сообщениями через /updates
GET /api/bot-api/getChatDiscussion?channelId=…

Что сейчас прицеплено к каналу — чтобы скрипт мог проверить, а не гадать.

Одна группа — одному каналу. Группа, прицепленная к двум каналам, собирала бы два потока комментариев без всякой возможности их различить, поэтому вторая привязка отвечает 409.

Профиль бота

POST /api/bot-api/setMyProfile

name и description — как бот представляется. Удобно править вместе с выкладкой новой версии, а не руками в настройках.

GET /api/bot-api/getMyStats

Сколько у бота чатов, сколько он отправил сообщений и со сколькими людьми пересекается.

Кнопки

keyboard — массив рядов, каждый ряд — массив кнопок. Нажатие отправляет боту обычное сообщение с текстом из data, так что обрабатывается оно там же, где и всё остальное. Поле можно назвать и action — сервер понимает обе формы.

await fetch(`${API}/sendMessage`, {
  method: "POST",
  headers: auth,
  body: JSON.stringify({
    chatId,
    text: "Что показать?",
    keyboard: [
      [{ text: "Погода", data: "/погода" }, { text: "Курс", data: "/курс" }],
      [{ text: "Помощь", data: "/help" }],
      // Кнопка, открывающая мини-приложение бота, а не отправляющая ответ:
      [{ text: "Открыть магазин", app: "https://example.com/app/shop" }],
      // Ссылка и цветная кнопка:
      [{ text: "Сайт", url: "https://example.com" }, { text: "Купить", data: "/buy", style: "success" }],
    ],
  }),
});

Кнопка с полем app открывает мини-приложение бота — адрес должен вести на тот же сайт, что указан в настройках приложения, иначе кнопка ничего не откроет. Кнопка с полем url (только http/https) открывает ссылку. Поле style красит кнопку: primary, success или danger.

Альбомы, кружки и копирование

Всё, что умеет показывать приложение, теперь умеет отправлять и бот. Файлы — ссылкой: своё хранилище у бота уже есть, а второй путь загрузки тянул бы за собой свои лимиты и чистку.

МетодПоляЧто делает
sendVideochatId, url, caption?, replyToId?Видео с проигрывателем.
sendVoicechatId, url, durationSec?, caption?Голосовое сообщение. Без durationSec шкала будет пустой.
sendVideoNotechatId, url, durationSec?Круглое видеосообщение.
sendMediaGroupchatId, items[], caption?Альбом до 10 файлов одним сообщением: [{ kind: "image", url }, …]. Виды: image, video, file, voice, video-note.
copyMessagechatId, messageIdОтправить содержимое чужого сообщения от своего имени, без пометки «переслано». Копировать можно только из чатов, где бот состоит.
setMyAvatarimageКартинка бота — ссылкой или data:-строкой. Имя и описание меняет setMyProfile.
await fetch(`${API}/sendMediaGroup`, {
  method: "POST",
  headers: auth,
  body: JSON.stringify({
    chatId,
    caption: "Новинки недели",
    items: [
      { kind: "image", url: "https://example.com/1.jpg" },
      { kind: "image", url: "https://example.com/2.jpg" },
      { kind: "video", url: "https://example.com/3.mp4" },
    ],
  }),
});

Мини-приложения

Мини-приложение — обычная веб-страница на вашем сервере, которая открывается прямо внутри Shalter, поверх переписки, и знает, кто её открыл. Это способ дать боту настоящий интерфейс: каталог, корзину, форму, игру — всё, для чего кнопок и текста мало.

Как включить

Настройки → Боты → ваш бот → Настройки бота → «Мини-приложение»: адрес страницы и надпись на кнопке. Или из кода:

POST /api/bot-api/setWebApp
await fetch(`${API}/setWebApp`, {
  method: "POST",
  headers: auth,
  body: JSON.stringify({ url: "https://example.com/app", name: "Магазин" }),
});

Только https (http разрешён для localhost — на нём и отлаживают). Пустой url убирает приложение. После этого кнопка появляется в шапке чата с ботом, и её же можно поставить в клавиатуру сообщения.

На странице

<script src="https://ваш-shalter/js/shalter-web-app.js"></script>
<script>
  const app = window.Shalter.WebApp;
  app.ready();                                  // убрать индикатор загрузки
  document.title = "Привет, " + app.user.name;  // кто открыл (без проверки — для вида)

  app.MainButton.setText("Оформить заказ").show();
  app.MainButton.onClick(async () => {
    app.MainButton.showProgress();
    await app.sendData(JSON.stringify({ items: 2 }));  // уйдёт боту, окно закроется
  });
</script>
Что естьОписание
initDataПодписанная строка — её и надо переслать боту для проверки. Целиком, не по частям.
userКто открыл: id, name, username, isPremium. Удобно для интерфейса, недостаточно для доверия.
colorScheme"light" или "dark" — тема, в которой человек сидит.
ready()Сообщить, что страница отрисовалась.
close()Закрыть окно.
sendData(данные)Отправить данные боту и закрыть окно. До 4096 символов.
openLink(url)Открыть внешнюю ссылку в настоящем браузере.
showAlert / showConfirmСообщение и вопрос «да/нет»; showConfirm возвращает промис с true/false.
MainButtonНижняя кнопка на всю ширину: setText, show, hide, enable, disable, showProgress, hideProgress, onClick.
sendData устроен честнее, чем в Telegram. Там такое сообщение видит только бот, у человека в переписке не появляется ничего. Здесь это обычное сообщение от самого человека — он видит в чате, что именно отправило приложение от его имени, а бот получает его тем же /updates, что и всё остальное.

Приложение без своего сервера

Хостинга может не быть вовсе — тогда страницу хранит и раздаёт сам Shalter. Присылаете HTML, получаете работающее приложение по адресу /app/<юзернейм бота>; скрипт shalter-web-app.js подставляется автоматически.

POST /api/bot-api/setWebAppCode
await fetch(`${API}/setWebAppCode`, {
  method: "POST",
  headers: auth,
  body: JSON.stringify({
    name: "Магазин",
    code: `
      <h1>Пиццерия</h1>
      <button id="b">Заказать</button>
      <script>
        const app = Shalter.WebApp;
        app.ready();
        document.getElementById("b").onclick = () => app.sendData("заказ: 1 пицца");
      </script>`,
  }),
});

Можно прислать и целый документ с <html>, и один фрагмент разметки — во втором случае Shalter обернёт его сам. Пустой code убирает приложение. То же самое поле есть в приложении: Настройки → Боты → Настройки бота → «Мини-приложение» → «Написать здесь».

Такая страница работает в песочнице. Она отдаётся с адреса Shalter, поэтому её код специально лишён прав «своего»: ни сессионной куки, ни доступа к /api от имени открывшего, ни общего с приложением хранилища (Content-Security-Policy: sandbox). Всё, что ей нужно, она получает через Shalter.WebApp — как и приложение на чужом домене. Свои запросы к своему серверу делать можно: обычный fetch по абсолютному адресу с CORS.

Проверка, кто открыл

initData — строка вида user=…&chat_id=…&bot_id=…&auth_date=…&hash=…. Всё, кроме hash, подписано ключом, который знают только Shalter и ваш токен, поэтому подделать имя нельзя, а поверить содержимому без проверки — можно только зря. Алгоритм тот же, что в Telegram Mini Apps:

import crypto from "node:crypto";

function checkInitData(initData, token) {
  const params = new URLSearchParams(initData);
  const hash = params.get("hash");
  const check = [...params.entries()]
    .filter(([k]) => k !== "hash")
    .sort(([a], [b]) => (a < b ? -1 : 1))
    .map(([k, v]) => `${k}=${v}`)
    .join("\n");
  const secret = crypto.createHmac("sha256", "ShalterWebAppData").update(token).digest();
  const mine = crypto.createHmac("sha256", secret).update(check).digest("hex");
  if (mine !== hash) return null;
  return JSON.parse(params.get("user"));
}

Не хотите считать сами — отдайте строку серверу:

POST /api/bot-api/checkWebAppData
const res = await fetch(`${API}/checkWebAppData`, {
  method: "POST",
  headers: auth,
  body: JSON.stringify({ initData }),
});
const { ok, user, chatId } = await res.json();   // ok: false — подпись не сошлась
Проверяйте на своём сервере, а не в браузере. Проверка в самой странице бессмысленна: тот, кто подделывает initData, подделает и её результат. Подпись действительна сутки — старую, подсмотренную в чужой истории браузера, сервер отвергнет.

Команды

Список команд задаётся в приложении (Настройки → Боты → иконка списка) в формате BotFather — по одной на строку:

start - Начать
погода - Прогноз на сегодня
help - Что я умею

Он показывается людям по кнопке «/» в чате с ботом. Сами команды приходят обычными сообщениями с текстом /погода — отдельного типа события для них нет.

GET /api/bot-api/getMyCommands
POST /api/bot-api/setMyCommands

То же самое из программы — удобно обновлять список вместе с выкладкой новой версии бота, а не руками. Ведущий слэш можно не писать, он всё равно срезается.

await fetch(`${API}/setMyCommands`, {
  method: "POST", headers: auth,
  body: JSON.stringify({ commands: [
    { command: "start", description: "Начать" },
    { command: "погода", description: "Прогноз на сегодня" },
  ]}),
});

Редактор внутри приложения

Настройки → Боты → иконка кода. Внутри — настоящий редактор (подсветка синтаксиса, автодополнение по Bot API, парные скобки, поиск, отмена), кнопка «Запустить» для проверки на тестовом сообщении и консоль с логами вашего бота.

Код исполняется на сервере, при каждом входящем сообщении. Токен, хостинг и цикл опроса не нужны вовсе.

handleMessage

Единственное требование к коду — определить эту функцию:

async function handleMessage(msg, bot) {
  if (msg.text === "/start") {
    return bot.send("Привет! Напишите что-нибудь.");
  }

  // Можно ходить в чужие API прямо отсюда
  const res = await fetch("https://api.example.com/weather?city=Moscow");
  const data = await res.json();

  await bot.send(`Сейчас ${data.temp}°`, {
    keyboard: [[{ text: "Обновить", data: "/погода" }]],
  });
}
Что доступноОписание
msgВходящее сообщение: id, chatId, senderId, text, createdAt.
bot.send(text, opts)Ответить в тот же чат. opts — те же keyboard и replyToId.
bot.sendTo(chatId, text, opts)Написать в другой чат, где бот состоит.
msg.type === "join_request"Новая заявка на вступление в канал/группу, где бот — админ: chatId, userId, userName, username, bio.
bot.approveJoin(userId, chatId?) / bot.declineJoin(userId, chatId?)Принять или отклонить заявку (по умолчанию — в чате из msg).
bot.joinRequests(chatId?)Список ожидающих заявок.
fetchОбычный fetch — запросы к внешним сервисам.
console.log / warn / errorПишет в консоль редактора и в журнал бота.
setTimeout / clearTimeoutЗадержки внутри обработчика.

Ограничения — читать до того, как писать

Поверхность намеренно узкая: require, process, файловая система, модули npm и глобальные объекты Node недоступны.

  • Состояние не сохраняется. Каждый вызов начинается с чистого листа: переменные не переживают следующее сообщение. Нужна память — храните её во внешнем сервисе через fetch, или пишите бота на своём сервере.
  • 300 мс непрерывной работы процессора. Ожидание (fetch, отправка) сюда не считается — у него свой потолок в 20 секунд на весь вызов. Ограничение жёсткое по неприятной причине: сервер однопоточный, и пока ваш синхронный код крутится, приложение не отвечает никому.
  • 20 секунд на вызов целиком. Медленный внешний API успеет ответить, зависший — нет.
Песочница — не защита от чужого кода. В документации Node про модуль vm сказано прямо: это не средство безопасности. Здесь он ограничивает случайный ущерб от вашего собственного кода — опечатки, бесконечного цикла, забытого await. Не вставляйте туда скрипт от незнакомца: это ровно то же самое, что запустить у себя чужой шелл-скрипт.

Писать в VS Code

Встроенный редактор — это редактор одного файла, а не среда разработки: без дерева проекта, отладчика, npm-пакетов, git и расширений. Если нужно всё это — пишите бота у себя в VS Code как обычную программу и подключайте его по HTTP API из первой половины страницы. Тогда доступно ровно всё, к чему вы привыкли: любые зависимости, точки останова, тесты, своя база, деплой куда угодно.

Разница только в том, где живёт программа. Формат сообщений и отправка одинаковые: bot.send() в редакторе и POST /sendMessage снаружи — это один и тот же вызов внутри.

Ошибки

КодКогда
400Пустой text в sendMessage.
401Нет заголовка Authorization или токен недействителен.
404Бот не состоит в этом чате либо чат не существует.
429Слишком часто. Опрос раз в 1–2 секунды в лимиты укладывается с запасом.

Ошибка внутри кода в редакторе не роняет ни сервер, ни бота: она попадает в журнал бота (Настройки → Боты → код → консоль), а следующее сообщение обрабатывается как ни в чём не бывало.

Объекты

Message

ПолеТипОписание
idстрокаИдентификатор сообщения — годится для replyToId.
chatIdстрокаЧат, куда отвечать.
senderIdстрокаКто написал.
textстрокаТекст. У сообщения с вложением может быть пустым.
createdAtстрока, ISO 8601Время. Оно же — следующее значение after.

Кнопка

ПолеТипОписание
textстрокаЧто написано на кнопке.
dataстрокаЧто придёт боту сообщением при нажатии.

Игры: SDK внутри игры

Полная инструкция по играм — на отдельной странице: shalter.ru/games.

Игра в Shalter — одна HTML-страница (меню «Игры» → «Мои игры» → «Создать игру»). Она запускается в изолированном окне без доступа к аккаунту и сайту, а с приложением общается через объект window.Shalter — его вставлять не нужно, он уже есть.

// очки — по ходу игры и в конце (рекорд сохранится сам)
Shalter.score(points);
Shalter.gameOver(points);

// кто играет и какая тема у приложения
console.log(Shalter.player.name, Shalter.theme.dark, Shalter.theme.accent);

// рекорды: лучший результат игрока и таблица (в чате, откуда открыли игру)
const best = await Shalter.getBest();
const top = await Shalter.getLeaderboard({ limit: 10 }); // [{ position, name, score, isMe }]

Shalter.share();            // отправить «мой результат — N» с карточкой игры в чат
Shalter.vibrate(40);        // вибрация на телефоне
Shalter.close();            // закрыть игру
ЧтоОписание
Shalter.score(n) / gameOver(n)Текущий и итоговый счёт. Сохраняется лучший результат — общий и в чате, где отправили карточку игры.
Shalter.player{ id, name } игрока.
Shalter.theme / Shalter.chat{ dark, accent } — тема приложения; { id } — чат, откуда открыли (или null).
await Shalter.getBest()Лучший результат игрока.
await Shalter.getLeaderboard({ limit, chat })Таблица рекордов; chat: false — общая, а не этого чата.
Shalter.share(text?)Отправить результат в чат вместе с карточкой игры.
Shalter.vibrate(ms), Shalter.close()Вибрация и закрытие игры.
localStorageРаботает как обычно — данные хранятся отдельно для каждой игры.

Game API

HTTP-методы для разработчика игры: обновлять код из своего репозитория или CI, смотреть статистику и ставить рекорды со своего сервера — так счёт нельзя накрутить в браузере. Токен — в редакторе игры, кнопка «🔑 API-токен» (показывается один раз; новый отзывает прежний).

curl -H "Authorization: Bearer game_ВАШ_ТОКЕН" https://shalter.ru/api/game-api/getStats

curl -X POST -H "Authorization: Bearer game_ВАШ_ТОКЕН" -H "Content-Type: application/json" \
  -d '{"userId":"u_…","score":1500}' https://shalter.ru/api/game-api/setScore
МетодПараметрыЧто делает
GET /getGame—Игра и статистика.
POST /updateGamecode, title, description, emoji, isPublicОбновить игру (переданные поля). Код — до 512 КБ.
GET /getStats—{ plays, players, topScore }.
GET /getScoreschatId?, limit?Таблица рекордов (общая или чата).
POST /setScoreuserId, score, chatId?, force?Записать результат. Без force — только если лучше прежнего; с force — как есть (сброс накрутки).
POST /deleteScoreuserIdУдалить результаты игрока.

Бот может отправить карточку игры в чат: POST /api/bot-api/sendGame с chatId и gameId (встроенные игры: builtin-2048, builtin-snake, builtin-flappy).

Адрес API на этом развёртывании — /api/bot-api от корня сайта. Вопросы и заявки на интеграцию — через страницу для партнёров.