# HTTP Bot API: supported methods This document lists every Bot API method that the telesrv HTTP gateway (`internal/botapi`, default port `8081`) actually implements, together with the parameters it accepts, the extensions it adds, and the official methods it deliberately does not support. Russian version of this document: [bot-api.ru.md](./bot-api.ru.md). ## How to use this document - [Transport](#transport) - [Authentication](#authentication) - [Request and response conventions](#request-and-response-conventions) - [Errors](#errors) - [Method summary](#method-summary) - [Updates](#updates) - [Methods: bot identity and commands](#methods-bot-identity-and-commands) - [Methods: sending](#methods-sending) - [Methods: rich messages](#methods-rich-messages) - [Methods: ephemeral messages](#methods-ephemeral-messages) - [Methods: editing and deleting](#methods-editing-and-deleting) - [Methods: files](#methods-files) - [Methods: webhooks](#methods-webhooks) - [Methods: menus, emoji status, web apps](#methods-menus-emoji-status-web-apps) - [Methods: payments](#methods-payments) - [Markup and entities](#markup-and-entities) - [Limits](#limits) - [Not implemented](#not-implemented) - [Demos and self-tests](#demos-and-self-tests) ## Transport | Item | Value | |---|---| | Method endpoint | `http(s):///bot/` | | File download | `http(s):///file/bot/` | | Listener setting | `TELESRV_BOT_API_ADDR`; an empty value disables the gateway | | Public address shown by BotFather | `TELESRV_BOT_API_PUBLIC_URL`; empty shows none | | Request body limit | 25 MiB upload + 1 MiB overhead (`FILE_TOO_BIG`) | | Accepted content types | `application/json`, `multipart/form-data`, `application/x-www-form-urlencoded` | `` is matched case-insensitively. Both `GET` and `POST` reach the dispatcher, so `urlencoded` GET requests work. Client configuration: - `python-telegram-bot`: pass `--base-url http://host:8081/bot` and `--base-file-url http://host:8081/file/bot`. - `aiogram`: `TelegramAPIServer.from_base("http://host:8081")`. - grammY: `new Api("host:8081")` / `api.config.use((ctx) => ({ apiRoot: "http://host:8081/bot" }))`. ## Authentication The path token must be the bot's `:` pair and its secret must match the value stored for that bot. Anything else returns HTTP `401` with `ACCESS_TOKEN_INVALID`. There is no separate auth header; the token in the URL is the credential, exactly like the official gateway. File downloads (`/file/bot...`) are authenticated the same way. ## Request and response conventions Success: ```json {"ok": true, "result": {}} ``` Failure: ```json {"ok": false, "error_code": 400, "description": "CHAT_ID_INVALID"} ``` `error_code` is the HTTP status code. `description` is an upper-case `SCREAMING_SNAKE` error marker, not always the official Telegram wording — match on the marker, not on the whole string. Field decoding is generic: JSON bodies are flattened to strings, non-string JSON values are re-marshalled to their JSON form, and `multipart` uses the first value of each field. This is why several methods accept both the `message_text` shorthand and the full `InputTextMessageContent` object, and why `setChatMenuButton` accepts both a nested `menu_button` object and a flat body carrying `type`. ## Errors Markers the gateway can return directly: | Marker | HTTP | Notes | |---|---:|---| | `ACCESS_TOKEN_INVALID` | 401 | bad or unknown token | | `METHOD_NOT_FOUND` | 404 / 501 | unknown method, or the feature is not wired in this deployment | | `BAD_REQUEST` | 400 | unclassified failure | | `CHAT_ID_INVALID` | 400 | `chat_id` missing, zero, or unparsable | | `MESSAGE_ID_INVALID` | 400 | `message_id` missing or `<= 0` | | `MESSAGE_IDENTIFIER_INVALID` | 400 | `inline_message_id` mixed with `chat_id`/`message_id` | | `MESSAGE_EMPTY` | 400 | no `text` given | | `MESSAGE_TOO_LONG` | 400 | over the text limit | | `MESSAGE_NOT_MODIFIED` | 400 | edit that changes nothing | | `REPLY_PARAMETERS_INVALID` | 400 | conflicting reply selectors | | `REPLY_MESSAGE_ID_INVALID` | 400 | negative or malformed reply target | | `MESSAGE_THREAD_ID_INVALID` | 400 | ephemeral top-message id out of range | | `ENTITY_INVALID`, `ENTITY_BOUNDS_INVALID`, `ENTITY_TYPE_UNSUPPORTED`, `ENTITIES_TOO_LONG` | 400 | entity list problems | | `FILE_ID_INVALID`, `FILE_TOO_BIG`, `MEDIA_INVALID` | 400 | file/media problems | | `BUTTON_INVALID`, `BUTTON_TYPE_INVALID`, `BUTTON_DATA_INVALID`, `BUTTON_URL_INVALID` | 400 | markup problems | | `RICH_MESSAGE_INVALID`, `RICH_MESSAGE_TOO_LONG`, `RICH_MESSAGE_DATE_INVALID`, `RICH_MESSAGE_BLOCKS_UNSUPPORTED`, `RICH_MESSAGE_MEDIA_UNSUPPORTED`, `RICH_MESSAGE_OPTION_UNSUPPORTED` | 400 | rich message problems | | `WEBPAGE_MEDIA_EMPTY` | 400 | rich HTML referenced remote media the local blob backend cannot materialize | | `EFFECT_ID_INVALID`, `VALUE_INVALID` | 400 | bad numeric parameter | | `BOT_COMMAND_INVALID` | 400 | malformed or oversized `commands` | | `BOT_COMMAND_SCOPE_UNSUPPORTED` | 400 | non-default scope, or any `language_code` | | `EPHEMERAL_MESSAGE_ID_INVALID`, `EPHEMERAL_TARGET_REQUIRED`, `EPHEMERAL_ACTION_EXPIRED` | 400 | ephemeral addressing problems | | `QUERY_ID_INVALID`, `USER_ID_INVALID`, `RESULT_ID_INVALID`, `RESULT_ID_EMPTY`, `RESULT_TYPE_INVALID` | 400 | query/user/result problems | | `OFFSET_INVALID` | 400 | `getUpdates` offset below `-10000` | | `ALLOWED_UPDATES_INVALID` | 400 | not a JSON array of up to 100 strings | | `ALLOWED_UPDATES_UNSUPPORTED` | 501 | gateway without the update-control service | | `MAX_CONNECTIONS_INVALID` | 400 | outside `1..100` | | `SECRET_TOKEN_INVALID` | 400 | characters outside `[A-Za-z0-9_-]`, or longer than 256 | | `WEBHOOK_URL_INVALID` | 400 | not an absolute `http(s)` URL, longer than 2048, has userinfo/fragment, or a port outside `1..65535` | | `CERTIFICATE_PINNING_UNSUPPORTED` | 400 | `certificate` / uploaded certificate supplied | | `IP_ADDRESS_UNSUPPORTED` | 400 | `ip_address` supplied | | `WEBHOOK_UNSUPPORTED` | 501 | gateway without the webhook service | | `METHOD_NOT_FOUND` | 501 | `answerShippingQuery` | | `BLOCKED_USER_EMOJI_STATUS_SERVICE_MISSING`, `BLOCKED_WEBAPP_QUERY_SERVICE_MISSING`, `BLOCKED_PREPARED_INLINE_SERVICE_MISSING` | 501 | optional service not wired | | `USER_PERMISSION_DENIED` | 403 | emoji status without the required user grant | | `PREMIUM_ACCOUNT_REQUIRED` | 400 | emoji status without Premium | | `PREMIUM_GIFT_SELF_INVALID`, `PREMIUM_GIFT_CODE_INVALID`, `BALANCE_TOO_LOW`, `STAR_COUNT_INVALID`, `MONTH_COUNT_INVALID` | 400 | premium gift problems | | `IDEMPOTENCY_KEY_INVALID` | 400 | conflicting `request_id`/`Idempotency-Key`, or bad charset/length | | `PAYMENT_FORM_INVALID` | 400 | payment form rejected | | `CHAT_WRITE_FORBIDDEN`, `CHAT_ADMIN_REQUIRED` | 400 | rights missing | | `USER_BOT_REQUIRED` | 400 | target is not a bot user | | `CONFLICT: ...` | 409 | concurrent poll or webhook delivery | | `INTERNAL_SERVER_ERROR` | 500 / 503 | store or transport failure | ## Method summary Implemented (40 methods + 1 file route): | Group | Methods | |---|---| | Bot & commands | `getMe`, `setMyCommands`, `deleteMyCommands`, `getMyCommands` | | Updates | `getUpdates` | | Text | `sendMessage`, `sendRichMessage` | | Media | `sendPhoto`, `sendAnimation`, `sendAudio`, `sendDocument`, `sendLivePhoto`, `sendSticker`, `sendVideo`, `sendVideoNote`, `sendVoice` | | Location | `sendContact`, `sendLocation`, `sendVenue` | | Edit/delete | `editMessageText`, `editMessageCaption`, `editMessageReplyMarkup`, `deleteMessage` | | Chat actions | `sendChatAction` | | Ephemeral edit/delete | `editEphemeralMessageText`, `editEphemeralMessageMedia`, `editEphemeralMessageCaption`, `editEphemeralMessageReplyMarkup`, `deleteEphemeralMessage` | | Callback | `answerCallbackQuery` | | Files | `getFile`, `GET /file/bot/` | | Webhooks | `setWebhook`, `deleteWebhook`, `getWebhookInfo` | | Menus & status | `setChatMenuButton`, `getChatMenuButton`, `setUserEmojiStatus` | | Web apps & inline | `answerWebAppQuery`, `savePreparedInlineMessage` | | Payments | `giftPremiumSubscription` | | Blocked by design | `answerShippingQuery` (HTTP 501) | ## Updates Only three update kinds are produced: | `allowed_updates` value | Update key | |---|---| | `message` | `message` | | `edited_message` | `edited_message` | | `callback_query` | `callback_query` | `allowed_updates` is a JSON array of up to 100 strings; unknown names parse successfully but never match anything, so do not rely on filtering to hide updates — drop the fields in the bot. The setting is accepted both by `getUpdates` and by `setWebhook`, and it persists per bot. Ephemeral (Bot API 10.2) messages arrive as normal `message` updates, but their `message_id` belongs to the ephemeral namespace rather than the chat's message numbering, so ordinary `reply_to_message_id` will not address them. Replies go back through `reply_parameters.ephemeral_message_id`; see [Methods: ephemeral messages](#methods-ephemeral-messages). Queue retention is controlled by `TELESRV_BOT_API_UPDATE_RETENTION` (default `24h`); acknowledged updates are dropped earlier. ## Methods: bot identity and commands ### `getMe` No parameters. Returns the bot's own `User` object. ### `setMyCommands` | Parameter | Type | Notes | |---|---|---| | `commands` | JSON array | up to 100 entries of `{command, description, is_ephemeral}` | | `scope` | object | only `{"type":"default"}` is accepted | | `language_code` | string | must be empty | `is_ephemeral=true` marks the command as an ephemeral one: invoking it in a group produces a message only the caller can see, and the bot must answer inside the ephemeral action window. Returns `true`. ### `deleteMyCommands` Same scope rules as `setMyCommands`; clears the list. Returns `true`. ### `getMyCommands` Same scope rules. Returns the stored array; entries include `is_ephemeral: true` only when the flag is set. ## Methods: sending ### `sendMessage` | Parameter | Type | Notes | |---|---|---| | `chat_id` | integer | required, non-zero; negative for groups/channels | | `text` | string | required, up to 4096 characters after formatting | | `parse_mode` | string | `HTML`, `Markdown`, `MarkdownV2`, or omitted for plain text | | `entities` | JSON array | used when `parse_mode` is absent; ignored otherwise | | `reply_markup` | object | any of the four constructors, see [Markup](#markup-and-entities) | | `disable_web_page_preview` | boolean | | | `disable_notification` | boolean | | | `reply_to_message_id` | integer | | | `receiver_user_id` | integer | **telesrv extension**: switches the call to an ephemeral send | | `callback_query_id` | integer | ephemeral send anchored to a callback query | | `reply_parameters` | object | `{ephemeral_message_id}` for ephemeral replies, `{message_id}` for ordinary ones | | `message_thread_id` | integer | ephemeral: the top message id of the discussion group | Ordinary and ephemeral addressing are mutually exclusive: with `receiver_user_id` set, `reply_to_message_id` must stay unset and `reply_parameters` may only carry `ephemeral_message_id`. Ephemeral sends accept inline keyboards only (`BUTTON_TYPE_INVALID` otherwise). The result is the same `Message` shape, with the ephemeral id in `message_id`. ### `sendPhoto`, `sendAnimation`, `sendAudio`, `sendDocument`, `sendSticker`, `sendVideo`, `sendVideoNote`, `sendVoice` All share one handler. | Parameter | Type | Notes | |---|---|---| | `chat_id` | integer | required | | `` | string / file | the method's own field: `photo`, `animation`, `audio`, `document`, `sticker`, `video`, `video_note`, `voice` | | `caption` | string | up to 1024 characters | | `parse_mode`, `caption_entities` | | as in `sendMessage` | | `reply_markup` | object | all four constructors | | `disable_notification`, `reply_to_message_id` | | | | `width`, `height`, `duration`, `title`, `performer` | integer / string | accepted and forwarded as metadata hints | | `emoji` | string | sticker only | | `receiver_user_id`, `callback_query_id`, `reply_parameters`, `message_thread_id` | | same ephemeral extension as `sendMessage` | The file field accepts, in this order of precedence: 1. `attach://` referencing a multipart file part; 2. an uploaded multipart part with the same field name; 3. an `http://` or `https://` URL (the server fetches it); 4. a `file_id` previously returned by `getFile`. Anything else is `FILE_ID_INVALID`. ### `sendLivePhoto` Two media fields: `photo` (still frame) and `live_photo` (the video part). Both accept upload, `attach://`, or `file_id`. The video part **cannot** be an HTTP URL — that is `FILE_ID_INVALID`, matching the official restriction. Captions, `parse_mode`, and the ephemeral extension work as for the other media methods. ### `sendContact`, `sendLocation`, `sendVenue` These three are **ephemeral-only**: they require `receiver_user_id` (or a `callback_query_id`) and otherwise fail with `EPHEMERAL_TARGET_REQUIRED`. `sendContact` takes `chat_id`, `phone_number`, `first_name`, optional `last_name`, and `vcard` (up to 2048 bytes), plus an optional inline `reply_markup`. `sendLocation` takes `chat_id`, `latitude` (`-90..90`), `longitude` (`-180..180`), `horizontal_accuracy` (`0..1500`, default `0`). A non-zero `live_period` is rejected with `MEDIA_INVALID` — live location is not implemented. `sendVenue` adds required `title` and `address`, and accepts either `foursquare_id` + `foursquare_type` or `google_place_id` + `google_place_type`. ## Methods: rich messages ### `sendRichMessage` Telesrv extension, Layer 228 rich text. Sends `InputRichMessage` instead of a plain formatted string, so tables, headings, dividers, details blocks and footers reach the client as real rich blocks. | Parameter | Type | Notes | |---|---|---| | `chat_id` | integer | required | | `rich_message` | object | `{"html": "..."}` **or** `{"markdown": "..."}`, plus optional `is_rtl` and `skip_entity_detection` | | `reply_markup` | object | inline keyboards only | | `reply_parameters` | object | `{"message_id": N}`; the legacy `reply_to_message_id` is also accepted, but not both | | `disable_notification`, `protect_content` | boolean | | | `message_effect_id` | integer | message effect id | Rules and rejections: - exactly one of `html` / `markdown` must be non-empty; zero or both is `RICH_MESSAGE_INVALID`; - `blocks` is parsed but rejected — `RICH_MESSAGE_BLOCKS_UNSUPPORTED`; - `media` is parsed but rejected — `RICH_MESSAGE_MEDIA_UNSUPPORTED`; - the source is capped at 256 KiB; - `business_connection_id`, `message_thread_id`, `direct_messages_topic_id`, `allow_paid_broadcast`, and `suggested_post_parameters` are rejected with an explicit error instead of being silently ignored; - rich HTML that references remote media (`img`, `video`, `audio`, `tg-map`, `tg-collage`, `tg-slideshow`) fails with `WEBPAGE_MEDIA_EMPTY`, because the local blob backend cannot materialize an arbitrary URL atomically. The documented workaround (see `cmd/bots/bedolagaformat`) is to retry once with the logo removed rather than degrade to a classic menu. Supported HTML elements: `b`/`strong`, `i`/`em`, `u`/`ins`, `s`/`strike`/`del`, `tg-spoiler`, `span` (only as ``), `a`, `code`, `pre` (with `class="language-…"`), `blockquote`, `tg-emoji`, `tg-time`, `footer`, and `table` (with the `bordered` and `striped` attributes, per-cell alignment). `tg-time` requires `unix` in `1..2^31-1` and a `format` built from `t`, `T`, `d`, `D`, `w`/`W`, or exactly `r`/`R` for a relative timestamp. Markdown input is parsed by the same rich builder used on the MTProto side, so `#`-style headings, `---` dividers, pipe tables, `>` quotes, fenced code and footer blocks all work. `editMessageText` accepts the same `rich_message` object, and there is no separate `editMessageRichMessage` method. ## Methods: ephemeral messages Ephemeral messages (Bot API 10.2) are visible only to their receiver, who sees a "only visible to you" notice. telesrv supports the whole lifecycle. Sending is done through the ordinary methods with `receiver_user_id`: - `sendMessage` — text; - the media methods — media plus a caption; - `sendContact`, `sendLocation`, `sendVenue` — ephemeral only. Addressing rules: - `receiver_user_id` is required and must be a positive integer; - `callback_query_id` may replace it as the anchor (the receiver is derived from the query); - `reply_parameters.ephemeral_message_id` replies to an existing ephemeral message, and cannot be combined with `callback_query_id`; - `reply_parameters.message_id` is the ordinary reply form and is not combined with a receiver; - `message_thread_id` is the top message of the ephemeral discussion group; - the action window is 15 seconds — after it the server answers `EPHEMERAL_ACTION_EXPIRED`. Editing and deleting: | Method | Parameters | |---|---| | `editEphemeralMessageText` | `chat_id`, `receiver_user_id`, `ephemeral_message_id`, `text`, `parse_mode`, `entities`, `reply_markup` | | `editEphemeralMessageCaption` | same, with `caption` + `caption_entities` | | `editEphemeralMessageMedia` | same, plus `media` (see below) | | `editEphemeralMessageReplyMarkup` | same, plus `reply_markup` only | | `deleteEphemeralMessage` | `chat_id`, `receiver_user_id`, `ephemeral_message_id` | `media` is an `InputMedia`-like object: `type` (one of `photo`, `animation`, `audio`, `document`, `video`, `live_photo`), the file field, and optional `caption`, `parse_mode`, `caption_entities`, `width`, `height`, `duration`, `title`, `performer`. `live_photo` uses `photo` for the still and `media` for the video. Raw uploads are not allowed here — only `file_id`, `attach://`-free file references, or HTTPS URLs (`FILE_ID_INVALID` otherwise). `reply_markup` is inline-only, and its *presence* matters: sending `reply_markup` with an empty value clears the keyboard. ## Methods: editing and deleting ### `editMessageText` The main edit method. It covers both ordinary and rich text edits, and both chat messages and inline messages. | Parameter | Type | Notes | |---|---|---| | `chat_id` + `message_id` | | one addressing mode | | `inline_message_id` | string | the other mode; must not be mixed with the first | | `text` | string | one content form | | `rich_message` | object | the other content form; must not be mixed with `text` | | `parse_mode`, `entities` | | as in `sendMessage` | | `reply_markup` | object | inline only; presence clears when empty | | `disable_web_page_preview` | boolean | | For an `inline_message_id` the result is a boolean; for a chat message it is the updated `Message`. An edit that changes nothing returns `MESSAGE_NOT_MODIFIED`. ### `editMessageCaption` | Parameter | Type | Notes | |---|---|---| | `chat_id` + `message_id` | | required; `inline_message_id` is not supported yet (`INLINE_MESSAGE_ID_UNSUPPORTED`) | | `caption` | string | up to 1024 characters; omitted or empty removes the caption | | `parse_mode`, `caption_entities` | | as in `sendPhoto` | | `reply_markup` | object | inline only; omitted keeps the keyboard, empty clears it | Only a photo or document message (photo, video, animation, audio, voice, file) has a caption; a text message returns `MESSAGE_HAS_NO_CAPTION`. Returns the updated `Message`. Works in private chats and in channels/supergroups. ### `editMessageReplyMarkup` `chat_id` + `message_id` (no `inline_message_id` yet) and an optional inline `reply_markup`. Only the keyboard changes; an omitted `reply_markup` removes it. Returns the updated `Message`. There is no `editMessageMedia`, `editMessageLiveLocation`, or `stopMessageLiveLocation` — use `editMessageText` (or the ephemeral variants) instead. ### `sendChatAction` | Parameter | Type | Notes | |---|---|---| | `chat_id` | integer | a private chat, or a channel/supergroup where the bot is a member | | `action` | string | `typing`, `upload_photo`, `record_video`, `upload_video`, `record_voice`, `upload_voice`, `upload_document`, `choose_sticker`, `find_location`, `record_video_note`, `upload_video_note` | | `message_thread_id` | integer | forum topic, optional | Returns `true`. The status is transient, like MTProto `messages.setTyping`: nothing is stored. `business_connection_id` is not supported. ### `deleteMessage` `chat_id` + `message_id`. Returns `true`. Deleting an already-deleted or foreign message returns `false` or an error, as with the official gateway. ### `answerCallbackQuery` | Parameter | Type | Notes | |---|---|---| | `callback_query_id` | string | required | | `text` | string | notification text | | `url` | string | | | `show_alert` | boolean | modal instead of toast | | `cache_time` | integer | seconds | Returns `true`. ## Methods: files ### `getFile` `file_id` must be a telesrv-issued id. The result always has `file_unique_id == file_id` and `file_path == file_id`, so the download URL is `/file/bot/`. Foreign or unknown ids are `FILE_ID_INVALID`. ### `GET /file/bot/` Streams the file, chunk by chunk, with the stored MIME type and `Content-Length`. The path is the `file_id`; a path with extra segments is `404 FILE_NOT_FOUND`. ## Methods: webhooks ### `setWebhook` | Parameter | Type | Notes | |---|---|---| | `url` | string | empty value deletes the webhook instead | | `secret_token` | string | `[A-Za-z0-9_-]`, up to 256 characters | | `max_connections` | integer | `1..100`, default `40` | | `allowed_updates` | JSON array | see [Updates](#updates) | | `drop_pending_updates` | boolean | | `certificate` (and any uploaded certificate part) is rejected with `CERTIFICATE_PINNING_UNSUPPORTED`; use a publicly trusted certificate. `ip_address` is rejected with `IP_ADDRESS_UNSUPPORTED` — the gateway uses the configured address list. Setting a webhook while `getUpdates` is long-polling returns `409 CONFLICT`. While a delivery is in flight, `setWebhook` waits up to 20 seconds for it to finish and returns `409 CONFLICT` only if the delivery is still running. Delivery behaviour: - one HTTP `POST` per update, with a **single `Update` object** as the body (not an array like `getUpdates` returns); - header `X-Telegram-Bot-Api-Secret-Token` carries the secret token; - `Content-Type: application/json`; any `2xx` counts as delivered; - redirects are never followed, so the secret cannot leak to another host; - up to `max_connections` updates are delivered in parallel, 16 bots and 64 concurrent HTTP requests overall; - failures retry with exponential backoff from 1s up to 5 minutes, plus a small per-bot jitter; - delivery is confirmed up to the first failed update, so nothing is skipped. ### `getWebhookInfo` Returns `url`, `has_custom_certificate` (always `false` here), `pending_update_count`, and — when a webhook is configured — `max_connections`, `allowed_updates`, `last_error_date`, `last_error_message`. ### `deleteWebhook` `drop_pending_updates` is optional. Like `setWebhook`, it waits up to 20 seconds for an in-flight delivery and returns `409` only if that delivery is still running. ## Methods: menus, emoji status, web apps ### `setChatMenuButton` / `getChatMenuButton` `menu_button` is `{"type": "default"}`, `{"type": "commands"}`, or `{"type": "web_app", "text": "...", "web_app": {"url": "..."}}`. A flat body that carries `type` at the top level is also accepted, which keeps older library serializers working. Setting returns `true`; getting returns the current button (never `null` — an unset button reads as `{"type": "default"}`). ### `setUserEmojiStatus` `user_id`, `emoji_status_custom_emoji_id`, `emoji_status_expiration_date`. Requires the user to have granted the bot permission to set their status (otherwise `403 USER_PERMISSION_DENIED`) and to be a Premium user (`PREMIUM_ACCOUNT_REQUIRED`). Returns `true`. ### `answerWebAppQuery` `web_app_query_id` plus `result`. Only `InlineQueryResult` of type `article` is supported: `id` (up to 64 characters), `title`, `description`, `url` (HTTPS only), `input_message_content` (or the `message_text` shorthand), `reply_markup` (inline only). Other result types are `RESULT_TYPE_INVALID`. The response contains `inline_message_id` when the answer produced one. ### `savePreparedInlineMessage` `user_id`, `result` (same `article` restriction), and the peer filters `allow_user_chats`, `allow_bot_chats`, `allow_group_chats`, `allow_channel_chats`. Returns `{id, expiration_date}`. ## Methods: payments ### `giftPremiumSubscription` Pays for a Premium gift out of the bot's Stars balance, through the same durable Stars pipeline as MTProto `payments.sendStarsForm`. | Parameter | Type | Notes | |---|---|---| | `user_id` | integer | recipient; self-gifts are rejected | | `month_count` | integer | `1..120` | | `star_count` | integer | positive, at most 10^15 | | `text` | string | up to 128 characters | | `text_parse_mode`, `text_entities` | | restricted entity set | | `request_id` | string | telesrv extension for idempotency | Send an `Idempotency-Key` HTTP header (or the local `request_id` field) — the allowed charset is `[A-Za-z0-9]` plus `-`, `_`, `.`, `:` up to 128 characters. Replaying the same key with the same request returns the stored result; replaying it with a different recipient, plan, price, or text is `IDEMPOTENCY_KEY_INVALID`. Requests without any key still work, but each retry charges again, so always send a key. ### `dropPendingUpdates` Implemented: discards the bot's unconfirmed queued updates and answers `true`. A bot that has fallen behind needs it, otherwise it spends forever working through a backlog while `pre_checkout_query`, which lives 10 seconds, quietly expires. ### `answerPreCheckoutQuery` Implemented. Before any Stars move, the payer waits for the bot's answer for **at most 10 seconds** (Telegram's requirement). The bot receives a `pre_checkout_query` update and answers with `pre_checkout_query_id`, `ok` and, when `ok=false`, an `error_message`. - `ok=true` — the Stars are charged and the bot receives `successful_payment`; - `ok=false` — the payment is cancelled and `error_message` is shown to the payer; - no answer within 10 seconds — the payment does not go through (`PAYMENT_FAILED`). The timeout never degrades into charging anyway: a silent bot means a refusal, otherwise pre-checkout would verify nothing. Only the bot that owns the order may answer; a stale `pre_checkout_query_id` gets `QUERY_ID_INVALID`. The currency and `invoice_payload` in the answer are not compared against what the bot recorded at `sendInvoice`, so that check stays on the bot side, same as in Telegram. ### `answerShippingQuery` Recognized, but answers HTTP `501 METHOD_NOT_FOUND`: a Stars payment has no shipping step, so there is nothing to answer. ## Markup and entities `reply_markup` accepts exactly one constructor: | Constructor | Notes | |---|---| | `inline_keyboard` | rows of buttons | | `keyboard` | reply keyboard; `resize_keyboard`, `one_time_keyboard`, `is_persistent`, `input_field_placeholder`, `selective` | | `remove_keyboard` | `remove_keyboard: true`, `selective` | | `force_reply` | `force_reply: true`, `input_field_placeholder`, `selective` | Inline keyboard buttons (exactly one action per button, otherwise `BUTTON_INVALID`): `url`, `callback_data` (up to 64 bytes), `web_app`, `login_url` (`url`, `bot_username`, `forward_text`, `request_write_access`), `switch_inline_query`, `switch_inline_query_current_chat`, `switch_inline_query_chosen_chat`, `copy_text`. Every button also accepts `text` (required), `style` (`primary` / `danger` / `success` / omitted), and `icon_custom_emoji_id`. Unknown fields are rejected rather than ignored. Reply keyboard buttons: a plain string, or an object with `text` plus one of `request_contact`, `request_location`, `request_poll` (`type`), `request_users` (`request_id`, `user_is_bot`, `user_is_premium`, `max_quantity`, `request_name`, `request_username`, `request_photo`), `request_chat` (`request_id`, `chat_is_channel`, `chat_is_forum`, `chat_has_username`, `chat_is_created`, `user_administrator_rights`, `bot_administrator_rights`, `bot_is_member`, `request_title`, `request_username`, `request_photo`), or `web_app`. Message entities (for `entities` / `caption_entities`, and for the `date_time` entity, which takes `unix_time` + `date_time_format`): `bold`, `italic`, `underline`, `strikethrough`, `code`, `pre` (`language`), `text_link` (`url`), `text_mention` (`user.id`), `spoiler`, `blockquote`, `expandable_blockquote`, `custom_emoji` (`custom_emoji_id`), `mention`, `hashtag`, `cashtag`, `bot_command`, `url`, `email`, `phone_number`, `bank_card_number`, `date_time`. Limits: 256 entities per message (`ENTITIES_TOO_LONG`), offsets in UTF-16 code units, unknown types are `ENTITY_TYPE_UNSUPPORTED`. ## Limits | Limit | Value | |---|---| | Text length | 4096 characters after parsing | | Caption length | 1024 characters | | Entities per message | 256 | | Inline result id | 64 bytes | | Callback data | 64 bytes | | Commands per `setMyCommands` | 100 | | Upload size | 25 MiB per file | | `allowed_updates` entries | 100 | | `getUpdates` `limit` | default and maximum 100 | | `getUpdates` `timeout` | clamped to 0..50 seconds | | `max_connections` | 1..100, default 40 | | Rich message source | 256 KiB | | `getFile` offset/limit | offset ignored; size reported in full | ## Not implemented Every other official method returns HTTP `404 METHOD_NOT_FOUND`. The most commonly reached ones: - editing: `editMessageMedia`, `editMessageLiveLocation`, `stopMessageLiveLocation`, `stopPoll`, and `inline_message_id` for `editMessageCaption` / `editMessageReplyMarkup`; - media groups and the rest of the `send*` surface: `sendMediaGroup`, `sendPoll`, `sendDice`, `sendGame`; - payments: `createInvoiceLink`, `sendInvoice`, `answerPreCheckoutQuery`, `answerShippingQuery`, `getStarTransactions`, `refundStarPayment`; - chat administration: `banChatMember`, `unbanChatMember`, `restrictChatMember`, `promoteChatMember`, `setChatPermissions`, `setChatPhoto`, `deleteChatPhoto`, `pinChatMessage`, `unpinChatMessage`, `unpinAllChatMessages`, `setChatTitle`, `setChatDescription`, `leaveChat`; - queries: `getChat`, `getChatAdministrators`, `getChatMember`, `getChatMemberCount`, `getForumTopic*`, `getUserChatBoosts`; - messages: `forwardMessage`, `forwardMessages`, `copyMessage`, `copyMessages`, `sendPoll`, `getFile`-adjacent helpers; - bot administration: `setMyName`, `setMyDescription`, `setMyShortDescription`, `setMyDefaultAdministratorRights`, `deleteWebhook` for other bots, `close`, `logOut`; - reactions, games, and business-account methods. MTProto clients are unaffected: everything implemented for the official Android/iOS/Desktop clients is reached over MTProto, not HTTP. See `docs/premium-stars.md` for the shared Stars and Premium pipelines. ## Demos and self-tests | Path | Purpose | |---|---| | `cmd/bots/ptbecho` | `python-telegram-bot` echo against the gateway, including an ephemeral command demo | | `cmd/bots/aiogramecho` | aiogram 3 echo, ephemeral replies, reply and inline keyboards | | `cmd/bots/bedolagaformat` | HTML / Markdown / MarkdownV2, rich menus via `sendRichMessage`, Telegram Login | | `cmd/bots/botcheck` | Go probe: imports the bot token over MTProto and checks the bot loop | | `cmd/bots/botdemo` | Go example: a bot sending to a channel or user on a timer | | `cmd/bots/grammystore` | Production grammY bot on the HTTP gateway (Postgres, own deployment) | | `scripts/botapi_echo_once.py` | Single-shot send/receive self-test | | `scripts/botapi_receive_selftest.py` | Update-queue and webhook self-test |