umbot
    Preparing search index...

    Platform contract check: official documentation ↔ umbot

    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:

    • ✅ — checked against the official documentation on 2026-08-29;
    • 📄 — according to the documentation recorded in AGENTS.md §9 (the page was unavailable on the day of the check — the fact is marked for re-checking);
    • ⚠️ — a discrepancy/nuance (stating whether it is a violation or not).

    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 session echo; 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 without session (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) 📄 ✅
    1. All response fields critical for operation match the documentation in names and nesting: 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.
    2. ⚠️ Cosmetic (types more precise than the runtime are not checked): the 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
    1. No contract violations were found. All required parameters, types and nesting match the official documentation; the limits are either 1:1 or more conservative than documented.
    2. ⚠️ Nuance-level discrepancies (not violations):
      • Alice: the 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.
      • VK messages.send: the message limit in the documentation is 9000, the framework truncates to 4096 (the historical bot message limit) — safe.
      • VK event_data ≤1000 and MAX secret [A-Za-z0-9_-]{5,256}: the framework validates more strictly than described on the overview pages — conservative.
      • Marusia: the state limit of 3584 bytes is hardcoded as a constant (the exact documented limit could not be checked — the VK docs were under maintenance).
    3. The platforms' responses to our requests were checked by read paths (section 8): all critical fields — 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.
    4. Checked live on 2026-08-29: VK (messages.send, users.get, sendMessageEventAnswer), Viber (the whole REST Bot API), MAX (the whole Bot API overview).
    5. Checked against the recorded documentation (the pages were temporarily unavailable): Telegram Bot API, the Alice protocol, Marusia, SmartApp — repeat the check using the links in this document once the pages are available.