This page is machine-translated from the Russian original. If something reads oddly, the Russian version is the source of truth — open an issue.
Check date: 2026-08-29. Framework version: the v-3.1.0 branch (HEAD 7194d8b).
For each platform action, the official contract (endpoint,
required parameters, limits) is compared with what the framework actually sends —
or, for webhook platforms, what the platform expects in the response versus
what getContent() returns.
Legend:
AGENTS.md §9 (the page was unavailable on the day
of the check — the fact is marked for re-checking);Source: Bot API 📄 (the page did not open on the day of the check; the facts are recorded in AGENTS.md §9 and covered by tests).
Common transport. Officially: POST https://api.telegram.org/bot<token>/<method>,
a JSON body. The framework: TelegramRequest.call() → the same URL (the token preferably from
initToken(), falling back to the config), JSON, timeouts of 5.5 s / 30 s for uploads ✅(📄).
| Action | Official contract | What the framework sends | Status |
|---|---|---|---|
| Webhook response | A plain HTTP 200 (the body is ignored) | HTTP 200, body ok; for unknown updates — skipAutoReply, no 5xx |
✅ |
sendMessage |
Required: chat_id, text (1–4096); parse_mode, reply_markup are optional |
POST bot<token>/sendMessage {chat_id, text (≤4096, resize), reply_markup (JSON), parse_mode — only when explicitly configured, dropped when the text is truncated}; an empty text is not sent |
📄 ✅ |
| Keyboards | inline_keyboard: [[{text, callback_data|url}]], callback_data 1–64 bytes, url and callback_data are mutually exclusive; keyboard: [[{text}]] |
buttonProcessing builds an array of arrays (rows by options._group/buttons.row(), up to 8 buttons per row, otherwise each button is its own row); the payload is serialized and checked against 64 bytes; url buttons have no callback_data; mixing inline+reply — a warning, inline is sent |
📄 ✅ |
sendPhoto |
chat_id, photo (URL/file_id/multipart), caption ≤1024 |
POST sendPhoto: photo = URL → string, file_id → string, local file → multipart; caption ≤1024 |
📄 ✅ |
sendMediaGroup |
media — an array of 2–10, attach:// for files in multipart |
POST sendMediaGroup (FormData): media JSON 2–10 with attach:// keys for local files; a single photo goes through sendPhoto |
📄 ✅ |
sendPoll |
chat_id, question ≤300, options 1–12, correct_option_ids for quizzes |
{chat_id, question (≤300), options (JSON [{text ≤100}] 1–12), correct_option_ids (index validation), explanation, ...} |
📄 ✅ |
answerCallbackQuery |
Required: callback_query_id; text ≤200 |
{callback_query_id, text (≤200), show_alert, url, cache_time} |
📄 ✅ |
answerInlineQuery |
inline_query_id, results |
{inline_query_id, results: [one article with message_text ≤4096]} |
📄 ✅ |
| Webhook secret | The X-Telegram-Bot-Api-Secret-Token header (a secret string comparison, not an HMAC of the body) |
isCorrectQuery: timingSafeEqual (constant time) with webhookSecret, opt-in |
📄 ✅ |
Sources: messages.send ✅, users.get ✅, messages.sendMessageEventAnswer ✅, Callback API 📄 (on the day of the check the VK sections were under maintenance).
Common transport. Officially: POST https://api.vk.ru/method/<method>, request
parameters, access_token, v. The framework: VkRequest.call() → x-www-form-urlencoded
(httpBuildQuery), access_token + v=5.199, a 5.5 s timeout (30 s for uploads).
| Action | Official contract | What the framework sends | Status |
|---|---|---|---|
| Webhook response (events) | HTTP 200 | HTTP 200, body ok |
✅ |
Response to confirmation |
The confirmation string from the community settings | sendInInit = the confirmation string (an adapter option or confirmation_token from the config) |
📄 ✅ |
messages.send ✅ |
Required: random_id; recipient peer_id/user_id/domain; message is required when there is no attachment; the message limit is 9000 |
{peer_id (or domain), message (≤4096, resize), random_id (int32), attachment (join ','), keyboard, template}; keyboard+template are mutually exclusive (template is removed with a warning); an empty message with no content is not sent |
✅ ⚠️ the framework truncates to 4096 — conservative, not a violation |
| VK keyboard | buttons: [[{action{type, label ≤40, payload ≤255, link}, color}]]; vkpay: hash inside action |
buttonProcessing: label ≤40, payload ≤255 (by code points), color; vkpay hash in action.hash, hash: null is not sent |
📄 ✅ |
users.get ✅ |
The parameter is user_ids (a string, a comma-separated list); user_id is not documented |
{user_ids: String(id)} for a number / user_ids: "a,b" for an array |
✅ |
messages.sendMessageEventAnswer ✅ |
Required: event_id, user_id, peer_id; event_data is an action object (show_snackbar/open_link/open_modal) |
{user_id (int, validated), event_id, peer_id (int), event_data (JSON ≤1000)} |
✅ ⚠️ the 1000 limit is not stated on the page — conservative |
| Uploading photos/documents | photos.getMessagesUploadServer → upload_url → multipart → photos.saveMessagesPhoto{photo, server, hash}; documents — docs.getMessagesUploadServer{peer_id, type} → docs.save{file, title, tags} |
The same sequence 1:1: getUploadServer → upload() (multipart) → save; the token is cached in ImageTokens/SoundTokens |
📄 ✅ |
| Webhook secret | The secret field in the body of every callback (when "Secret key" is enabled in the group) |
isCorrectQuery: timingSafeEqual of the body secret with secret_key, opt-in |
📄 ✅ |
Source: REST Bot API ✅ — checked against the live documentation on 2026-08-29.
Common transport. Officially: POST https://chatapi.viber.com/pa/<method>, JSON,
the X-Viber-Auth-Token header, body size ≤30 KB, a successful response has status === 0.
The framework: ViberRequest.call() → 1:1, plus a 30 KB check before sending and normalization of
min_api_version.
| Action | Official contract | What the framework sends | Status |
|---|---|---|---|
send_message (text) ✅ |
Required: receiver, type, sender.name ≤28; for text — text ≤7000; optional keyboard, min_api_version, tracking_data |
{receiver, sender{name ≤28 (or avatar)}, type:'text', text ≤7000 (resize), keyboard (Type:'keyboard', Buttons[]), min_api_version} |
✅ |
send_message (rich_media) |
type:'rich_media', rich_media{Type, ButtonsGroupColumns, ButtonsGroupRows, BgColor, Buttons[]}; Columns/Rows — a share of the 6×7 grid |
{..., type:'rich_media', min_api_version ≥7, rich_media{Type:'rich_media', ButtonsGroupColumns:6, ButtonsGroupRows:7, BgColor:'#FFFFFF', Buttons[{Text, ActionType, ActionBody, Columns, Rows, ...}]}}; the element text is in Text (HTML with escaping) |
✅ |
set_webhook ✅ |
url (required), event_types, send_name, send_photo; after the call Viber sends a verification callback and waits for HTTP 200 |
{url, event_types[delivered, seen, failed, subscribed, unsubscribed, message, conversation_started], send_name:true, send_photo:true}; the webhook event in setQueryData → skipAutoReply → HTTP 200 |
✅ |
get_user_details ✅ |
{id} |
{id} |
✅ |
| Incoming signature ✅ | X-Viber-Content-Signature = HMAC-SHA256(auth_token, request body) |
BasePlatform.isCorrectQuery: HMAC-SHA256 of the raw body (webhookHandle passes a string), timingSafeEqual |
✅ |
| Webhook response | HTTP 200 | HTTP 200, body ok |
✅ |
Source: Bot API ✅ — checked against the live documentation on 2026-08-29.
Common transport. Officially: base URL https://platform-api2.max.ru, authorization via
Authorization: <token> (query parameters are no longer supported), a limit of 30 rps.
The framework: MaxRequest.call() → 1:1, a 500 ms per-dialog queue with .unref() timers.
| Action | Official contract | What the framework sends | Status |
|---|---|---|---|
| Webhook response | HTTP 200 | HTTP 200, body ok; unknown update_type → skipAutoReply |
✅ |
POST /messages ✅ |
Body {text, attachments[]}; recipient user_id/chat_id (query); ≤12 attachments (the keyboard counts as an attachment); keyboard ≤30 rows × 7 buttons |
POST /messages?{user_id|chat_id}={id} body {text ≤4000 (resize), attachments ≤12 (media + inline_keyboard)}; buttons {type: message|link|callback|request_contact|request_geo_location|open_app, text required} — rows by options._group/buttons.row() (up to 7 buttons, 3 with link/open_app/geo/contact), without a group each in its own row; an interval of ≥500 ms between messages to a dialog |
✅ |
POST /answers ✅ |
A response to a callback | POST /answers?callback_id={id} body {} (acknowledging the press; the reply goes as a separate POST /messages) or {message} with max_callback_edit_message: true / api.answerCallback(text) (message replaces the message with the button); a per-dialog interval |
✅ |
POST /uploads ✅ |
Returns token (and url); token → attachment |
POST /uploads?type=image|video|audio|file → a multipart POST to the received url → the attachment gets token (or url for image); a 30 s upload timeout |
✅ |
POST /subscriptions ✅ |
{url, update_types, secret}; HTTPS is required |
{url (validated: https, port 443), update_types, secret}; secret is additionally validated by [A-Za-z0-9_-]{5,256} |
✅ |
| Webhook secret | The x-max-bot-api-secret header with the secret from the subscription |
isCorrectQuery: plain timingSafeEqual with webhookSecret, opt-in |
📄 ✅ |
Sources: protocol,
ItemsList ✅
(checked earlier in 2026-08: the limits and footer.button are confirmed). The general response format page
returned 404 on the day of the check — the fields below follow the recorded documentation 📄.
There are no outgoing requests for a regular response — the platform waits for HTTP 200 with JSON. "Expects ↔ we return" check:
| Response field | Official contract | What the framework returns | Status |
|---|---|---|---|
version |
"1.0" |
VERSION = '1.0' |
📄 ✅ |
response.text |
≤1024; may be empty only when tts is filled in |
Text.resize(controller.text, 1024); a warning on empty text+tts |
📄 ✅ |
response.tts |
≤1024 characters, <speaker>/sil do not count towards the limit |
resizeAlisaTts — the limit is by visible text, tags are not broken |
📄 ✅ |
response.card |
BigImage / ItemsList (1–5) / ImageGallery (1–10); limits: title 128, description 256/1024, header.text/footer.text/button.text 64, button.url 1024 bytes, button.payload 4096 bytes; footer = {text, button}; image_id is not required |
cardProcessing (Alisa/Card.ts): the same types, limits 1:1, text elements without an image are not dropped |
✅ |
response.buttons |
[{title ≤64, url ≤1024 bytes, payload ≤4096 bytes, hide}] |
buttonProcessing: the same byte limits |
📄 ✅ |
response.end_session |
boolean | controller.isEnd |
📄 ✅ |
session (echo) |
The example in the documentation contains session {session_id, message_id, user_id} |
Not returned | ⚠️ see note 1 |
user_state_update / application_state / session_state |
≤1 KB | A Buffer.byteLength ≤ 1024 check (ALISA_STATE_MAX_BYTES), otherwise the field is not sent and logError is called |
📄 ✅ |
directives.start_account_linking |
When requesting authorization | Added when controller.isAuth && userToken === null |
📄 ✅ |
Health check (original_utterance === 'ping') |
The response must contain pong | sendInInit = {version, response:{text:'pong'}} without calling the business logic |
📄 ✅ |
Note 1 (⚠️ the only discrepancy found with the documentation example). The official response example includes the
sessionecho; the framework does not send it. The response format page was unavailable on the day of the check, so whether the field is required could not be confirmed; empirically Yandex accepts a response withoutsession(skills on umbot pass the health check and moderation). This is not a confirmed violation — an item for a targeted check once the documentation is available.
| Action | Official contract | What the framework sends | Status |
|---|---|---|---|
| Image quota check | GET https://dialogs.yandex.net/api/v1/status, Authorization: OAuth <token> |
YandexImageRequest.checkOutPlace() → the same URL |
📄 ✅ |
| Image upload | POST .../skills/{skill_id}/images — {url} for remote, multipart for a file; DELETE .../images/{image_id} |
1:1 (downloadImageUrl / downloadImageFile with an extension whitelist / deleteImage), a 15 s timeout |
📄 ✅ |
| TTS | POST https://tts.api.cloud.yandex.net/speech/v1/tts:synthesize, {text, lang, voice, format, speed, emotion} → audio |
1:1 (YandexSpeechKit), oggopus, the folderId parameter is optional |
📄 ✅ |
Source: the Marusia protocol (dev.vk.com/ru/marusia/protocol) 📄 — on the day of the check the VK sections were under maintenance; the facts follow the recorded documentation and the Marusia API methods (dev.vk.com/ru/marusia/api).
| Response field | Official contract | What the framework returns | Status |
|---|---|---|---|
version |
"1.0" |
VERSION = '1.0' |
📄 ✅ |
session |
Echo: {session_id, message_id, user_id} |
result.session — an echo from the request (falling back to empty values) |
📄 ✅ |
response.text / tts |
≤1024; tts is counted without <speaker>/sil |
1:1 as in Alice (resizeMarusiaTts) |
📄 ✅ |
response.card |
BigImage {type, image_id: int}; ItemsList {type, items: [{image_id: int}]} |
cardProcessing (Marusia/Card.ts): 1:1 (gallery → ItemsList), tokens via marusia.getPictureUploadLink → upload → marusia.savePicture |
📄 ✅ |
response.buttons / end_session |
As in Alice | 1:1 | 📄 ✅ |
user_state_update / session_state |
A state limit | A ≤3584 bytes check (MARUSIA_STATE_MAX_BYTES) | ⚠️ conservative (the docs page was unavailable for an exact limit check) |
| Health check | ping → pong | sendInInit with pong |
📄 ✅ |
Marusia's outgoing API requests: marusia.getPictureUploadLink, marusia.savePicture{photo, server, hash},
marusia.getAudioUploadLink, marusia.createAudio{audio_meta}, marusia.deletePicture/{Audio}{id} —
through the common VK transport (api.vk.ru/method/, access_token, v), 1:1 with
dev.vk.com/ru/marusia/api. 📄 ✅
Source: the SmartApp protocol (salute.sber.ru, SmartApp API) 📄 — on the day of the check the portal was unavailable for automated checking (a redirect to developers.sber.ru); the facts follow the recorded documentation.
| Response field | Official contract | What the framework returns | Status |
|---|---|---|---|
messageName |
ANSWER_TO_USER (a reply), CALL_RATING (a rating) |
1:1 (getContent / getRatingContext) |
📄 ✅ |
sessionId, messageId, uuid |
An echo from the request | An echo from platformOptions.session |
📄 ✅ |
payload.pronounceText / pronounceTextType |
application/text or application/ssml |
application/ssml only when real SSML tags are present (the regex <\/?[a-z][^>]*>), otherwise application/text |
📄 ✅ |
payload.items[].bubble.text |
≤250 | Text.resize(text, 250) |
📄 ✅ |
payload.items[].command |
close_app on ending |
Added when controller.isEnd |
📄 ✅ |
payload.suggestions.buttons |
A list of buttons with actions (server_action/text/deep_link) |
≤8 buttons; payload → server_action{action_id, parameters}, url → deep_link |
📄 ✅ |
| Card | list_card (cells: image_cell_view/text_cell_view) |
1:1 (SmartApp/Card.ts), URL images only |
📄 ✅ |
| Data storage | GET/POST {storage_url}/{userId} (SmartApp Code tools/api/data) |
The URL is built with encodeURIComponent(userId) — protection against path traversal (an untrusted uuid.userId) |
📄 ✅ |
| Webhook response | HTTP 200 | HTTP 200, a JSON response | 📄 ✅ |
A separate check for name mismatches (imageId vs image_id): for each
outgoing request — the response format according to the documentation and the actual read path in the code.
The response envelope: {ok: boolean, result?: T, error_code?: number, description?: string}
(Bot API docs). The framework: TelegramRequest.call() checks data.data.ok, returns
data.data.result — names 1:1. 📄 ✅
| Request | Platform response | What the framework reads | Status |
|---|---|---|---|
sendPhoto (to cache file_id) |
result.photo[] — an array of sizes, each with file_id (snake_case) |
photo?.ok && photo.result?.photo?.length → result.photo[length-1].file_id → ImageTokens.imageToken |
📄 ✅ |
sendAudio (caching) |
result.audio.file_id |
sound.result?.audio?.file_id → SoundTokens.soundToken |
📄 ✅ |
| Other methods | result (message/poll/...) |
No caching needed — result is returned to the calling code | 📄 ✅ |
The response envelope: {response: T} or {error: {error_code, error_msg, ...}}
(VK API docs). The framework: VkRequest.call() returns data.data.response (or the whole
data.data if response is empty), an error → null. ✅
| Request | Platform response | What the framework reads | Status |
|---|---|---|---|
photos.getMessagesUploadServer |
{response: {upload_url, ...}} |
server?.upload_url → the upload URL |
📄 ✅ |
| Photo upload (multipart to upload_url) | {photo: "...", server: N, hash: "..."} |
upload?.photo, upload.server, upload.hash → photos.saveMessagesPhoto |
📄 ✅ |
photos.saveMessagesPhoto |
response: [{id, owner_id, ...}] — an array |
photo?.[0]?.id, photo[0].owner_id → token photo{owner_id}_{id} |
📄 ✅ |
docs.getMessagesUploadServer |
{response: {upload_url}} |
server?.upload_url |
📄 ✅ |
| Document upload | {file: "..."} |
uploadResponse.file → docs.save |
📄 ✅ |
docs.save |
response: {id, owner_id, title, ...} |
doc.owner_id, doc.id → token doc{owner_id}_{id} |
📄 ✅ |
users.get |
response: [{id, first_name, last_name, ...}] |
users[0] → first_name/last_name → the VK name cache |
✅ |
messages.send |
response: <message_id: number> |
Returned to the caller (messages are fire-and-forget) | 📄 ✅ |
messages.sendMessageEventAnswer |
response: true |
Returned to the caller | 📄 ✅ |
The response envelope: {status: 0, status_message: "ok", ..., failed_list?: []} (REST Bot
API docs ✅). The framework: ViberRequest.call() — success is data.status === 0, failed_list
is logged, status_message !== 'ok' → an error. Names 1:1.
| Request | Platform response | What the framework reads | Status |
|---|---|---|---|
send_message / set_webhook / get_user_details |
{status, status_message, chat_hostname?, message_token?} |
status === 0 → success; otherwise a log entry |
✅ |
get_user_details |
+ {id, name, avatar, country, language, primary_device_os, api_version, viber_version...} |
IViberGetUserDetails (snake_case 1:1), a public API |
✅ |
The response envelope: JSON without a common envelope; errors are an HTTP code/body (Bot API docs ✅).
The framework: MaxRequest.call() returns data.data as is.
| Request | Platform response | What the framework reads | Status |
|---|---|---|---|
POST /uploads ✅ |
{url, token, ...} — the token for sending the attachment |
uploadTarget?.url (where to upload the file), uploadTarget.token (the attachment) → IMaxUploadFile{url, token?}; the card/sound code reads upload?.token || upload?.url |
✅ |
POST /messages |
{message: {...}} |
Returned to the caller (fire-and-forget) | ✅ |
POST /answers |
A body without a strict schema (IMaxAppApi — {[name]: unknown}) |
Returned to the caller | ✅ |
The envelope: {<resource>: {...}} without a common error field; an error is an HTTP code + {message...}
(docs: resource-upload 📄).
| Request | Platform response | What the framework reads | Status |
|---|---|---|---|
GET /status |
{images: {total, used}, sounds: {...}} — quotas |
query?.images?.quota → IYandexCheckOutPlace{total, used} |
📄 ✅ |
POST skills/{id}/images |
{image: {id, ...}} |
query?.image?.id → ImageTokens.imageToken |
📄 ✅ |
DELETE skills/{id}/images/{imageId} |
{result: "ok"} |
query?.result |
📄 ✅ |
POST skills/{id}/sounds |
{sound: {id, ...}} |
query?.sound?.id → SoundTokens.soundToken |
📄 ✅ |
SpeechKit tts:synthesize |
Binary audio (oggopus) | ArrayBuffer → a temporary file → upload to the skill → unlink |
📄 ✅ |
⚠️ Cosmetic (does not affect operation): IYandexRequestDownloadImage declares
origUrl/size/createdAt in camelCase — the actual case of these fields in the Yandex response
(except id) is not used in the code and was not checked. If these fields are ever needed, check
their case against the documentation; image.id, which is critical for operation, matches exactly.
The envelope is the same as VK's ({response: ...}). The framework: via VkRequest.call().
| Request | Platform response | What the framework reads | Status |
|---|---|---|---|
marusia.getPictureUploadLink |
{response: {picture_upload_link}} (snake_case) |
uploadLink.picture_upload_link |
📄 ✅ |
| Image upload | {photo, server, hash} |
1:1 → marusia.savePicture |
📄 ✅ |
marusia.savePicture |
{response: {app_id, photo_id}} |
picture?.photo_id → ImageTokens.imageToken |
📄 ✅ |
marusia.getAudioUploadLink |
{response: {audio_upload_link}} |
audio_upload_link (interface; the speech flow uses Marusia's built-in sounds) |
📄 ✅ |
marusia.createAudio |
{response: {id, ...}} |
id (interface) |
📄 ✅ |
file_id (Telegram), id/owner_id (VK photos/docs), status: 0
(Viber), url/token (MAX uploads), image.id/sound.id (Yandex),
picture_upload_link/photo_id (Marusia). Not a single case of "we expect camelCase but
snake_case arrives" in the paths that are used.IVkUploadFile fields
(file/photo/server/hash) are declared optional — this matches
the non-overlapping subsets of real responses (photos: {photo, server, hash};
docs: {file}), the runtime checks are correct; IYandexRequestDownloadImage.origUrl/createdAt —
camelCase, the fields are not read.| Platform | Official contract | What the framework sends (the adapter's getUpdates) |
|---|---|---|
| Telegram | getUpdates: offset, timeout; does not work with an active webhook (409) |
POST getUpdates {timeout: 25, offset: last update_id + 1}; 401/404/409 — polling stops with a reason, other errors are retried; deleteWebhook — only with the telegram_delete_webhook option |
| VK | groups.getLongPollServer(group_id) → {server, key, ts}; a_check with key, ts, wait; failed 1/2/3 |
The community ID — groups.getById; GET {server}?act=a_check&key&ts&wait=25; failed: 1 — a new ts, 2 — a new key, 3 — a new key and ts |
| MAX | GET /updates: limit 1–1000, timeout 0–90 s, marker; Authorization: <token>; a webhook is recommended for production |
GET /updates?timeout=30[&marker], the marker from the response goes into the next request; 401 — polling stops |
session echo is not returned (the documentation example contains it; whether it is required
is not confirmed — the page returned 404 on the day of the check; the platform accepts it). An item for
a targeted check.messages.send: the message limit in the documentation is 9000, the framework truncates to 4096
(the historical bot message limit) — safe.event_data ≤1000 and MAX secret [A-Za-z0-9_-]{5,256}: the framework validates
more strictly than described on the overview pages — conservative.file_id (Telegram), id/owner_id (VK), status: 0 (Viber),
url/token (MAX), image.id/sound.id (Yandex), picture_upload_link/photo_id
(Marusia) — match in names and nesting. Not a single case of "we expect camelCase,
snake_case arrives" in the paths that are used; two cosmetic inaccuracies in the types
(they do not affect operation) are noted in section 8.messages.send, users.get,
sendMessageEventAnswer), Viber (the whole REST Bot API), MAX (the whole Bot API overview).Full reference — API v-3.1 · all versions.