Editorial

Telegram Bot API 10.3: Ephemeral Messages in Groups

Bot API 10.3 ephemeral messages: send a group reply only one member sees, the 15-second rule, and why message_id is 0.

JJyoti Ranjan SwainUpdated
Telegram Bot API 10.3 ephemeral messages, showing a bot reply visible to a single group member

Telegram shipped three releases of the Bot API in three months, and one of them quietly fixed the most annoying problem in group bots: the bot that answers one person and spams everyone else.

That feature is called ephemeral messages. It landed in Bot API 10.2 on July 14, 2026, and got a cleaner API in 10.3 on August 24. If you maintain a group bot, this is the update worth reading, because it changes how a reply reaches a user rather than adding another optional field.

Table of contents

What shipped, and when

Five releases since March, and the pace is the story as much as the features are.

Timeline of Telegram Bot API releases from 9.5 in March 2026 through 10.3 in August 2026

VersionDateHeadline change
9.5March 1, 2026date_time message entity, chat member tags
9.6April 3, 2026Managed bots, quizzes with multiple correct answers
10.0May 8, 2026Guest mode, live photos, media in polls
10.1June 11, 2026Rich messages, join request queries
10.2July 14, 2026Ephemeral messages and commands, Communities
10.3August 24, 2026EphemeralMessageParameters, disabled buttons, welcome message rights

Bot API 9.4, for the record, was February 9, 2026. I mention it because a few roundups floating around have the 9.4 date wrong, and the changelog on core.telegram.org is the only version I would trust.

Ephemeral messages, explained properly

An ephemeral message is a bot reply in a group that only one member can see. Telegram's own description is that it lets a bot and an individual member communicate privately on the public timeline without cluttering the chat for everyone else. The messages may disappear on their own after a while, or when the client app restarts.

The classic case: someone taps a button on your bot's message in a 4,000-member group. Before this, your options were to edit the shared message, which everyone sees, send a new group message, which everyone sees, or DM the user, which fails if they never started a private chat with your bot. Now you send it to that one person, in place.

You do it by passing ephemeral_message_parameters with a receiver_user_id:

json
{
  "chat_id": -1001234567890,
  "text": "Your ticket number is 4821.",
  "ephemeral_message_parameters": {
    "receiver_user_id": 987654321,
    "callback_query_id": "4382bfdwdsb323b2d9"
  }
}

Delivery is not guaranteed. The docs say so directly, and they repeat it for the offline case. Treat an ephemeral message as a courtesy layer, not as the record. Anything that must survive belongs in your database or in a regular message.

There is a third field worth knowing: replace_callback_query_message. Pass True and the ephemeral message appears in place of the original message for that user. It must be False for callback queries that came from an ephemeral message, and those you edit with the editEphemeralMessage… methods instead.

The 15-second rule that trips people up

This is the part that will generate bug reports. Any bot can send an ephemeral message to a user within 15 seconds of an eligible incoming action, and it goes to the exact client app that triggered the action. To do it, the bot has to supply one of two things:

  • the callback_query_id from a callback query it received, or
  • the reply_parameters.ephemeral_message_id from an incoming ephemeral message.

Two paths to sending an ephemeral message: a 15-second reply window for any bot, versus an open window for chat administrators

Administrators get a much better deal. If your bot is a chat administrator, it can send an ephemeral message to any non-bot member at any time, with no callback_query_id and no ephemeral_message_id. The tradeoff is that the message may go to several of that user's active client apps, and it still is not guaranteed to arrive at any of them.

So if your bot does slow work, a database lookup, an external API call, an image render, do not plan to return the result as an ephemeral message 40 seconds later. Either acknowledge inside the window, or make the bot an admin.

Ephemeral commands go the other direction

The same release added is_ephemeral to BotCommand. Set it to True and a user can send that command in a group where it is invisible to everyone else in the chat, including other bots. Only the sender and the target bot see it.

json
{
  "commands": [
    {"command": "balance", "description": "Check your balance", "is_ephemeral": true},
    {"command": "help", "description": "Show help"}
  ]
}

/balance in a group now costs the group nothing. description still has to be 1-256 characters, same as before.

The 10.2 to 10.3 breaking change

If you shipped ephemeral support in July, read this bit twice. In 10.2, receiver_user_id and callback_query_id were separate top-level parameters on sendMessage, sendPhoto, sendVideo, sendDocument, sendAnimation, sendAudio, sendSticker, sendVoice, sendVideoNote, sendLivePhoto, sendContact, sendLocation and sendVenue.

In 10.3 they were replaced by the single ephemeral_message_parameters object, which holds receiver_user_id, the optional callback_query_id, and the optional replace_callback_query_message.

json
// 10.2 style
{"chat_id": -100123, "text": "hi", "receiver_user_id": 987654321}

// 10.3 style
{"chat_id": -100123, "text": "hi",
 "ephemeral_message_parameters": {"receiver_user_id": 987654321}}

Six weeks between the two shapes. If you wrote against the July docs, your payloads use the old keys.

message_id is 0, and that will break your code

Here is the detail I would put on a sticky note. In the Message object, message_id is documented as unique inside the chat, and then: 0 for ephemeral messages.

Plenty of bots use message_id as a truthy check, a dictionary key, or a primary key. A zero walks straight through if (message_id) in JavaScript and if message_id: in Python. To identify an ephemeral message you use ephemeral_message_id, which exists on both Message and ReplyParameters. And in ReplyParameters, message_id is now optional when ephemeral_message_id is present.

The same 0 also appears for a different reason: a message the server schedules instead of sending immediately, such as a video sent to a very large chat. That message is unusable until it is actually sent. So message_id == 0 has two possible meanings, and neither one is an error you can ignore.

Rich messages and streaming AI replies

Rich messages arrived in 10.1 as a structured document format for bot output. Instead of one text blob with entity offsets, you send blocks: paragraphs, section headings, tables, block quotations, lists, collages, slideshows, maps, and one called RichBlockThinking. 10.3 added expandable block quotations, documents inside rich messages, and a is_compact flag on tables.

The interesting method is sendRichMessageDraft. It streams a partial rich message while the output is still being generated, which is the LLM token-streaming case written into the protocol. Two constraints matter:

  • the draft is a temporary 30-second preview, and it does not persist
  • when generation finishes you must call sendRichMessage with the complete message to save it in the chat

Drafts with the same draft_id animate between updates. A different draft_id replaces the draft with no animation. draft_id must be non-zero, and sendRichMessageDraft only targets private chats.

10.3 also added MessageGenerationStopped to Update, plus can_stop and keep_on_stop on the draft methods, so a user can hit stop mid-generation and your bot finds out.

Communities

10.2 introduced initial support for Communities: several supergroups, channels and bots linked together around a shared topic or audience. The Community object is deliberately thin, just id and name, with the ID documented as up to 52 significant bits, so store it in a 64-bit integer.

Three service messages come with it: CommunityChatAdded when a chat or bot is added, CommunityChatRemoved when it is removed, which carries no payload at all, and CommunityChatJoined in 10.3 for a chat being joined by a user from a community. ChatFullInfo gained a community field, so you can ask whether a chat belongs to one.

You still need a chat ID first

None of this works without the group's chat ID, and that is still the step people get stuck on. Supergroup IDs are negative and carry the -100 prefix, which trips up anyone parsing them as plain positive integers, and getUpdates refuses to return anything while a webhook is active.

Our Telegram Chat ID Finder takes a bot token, validates it, names the bot back to you so you know you pasted the right one, and lists the chat, group and channel IDs it can see. It handles the -100 form and the webhook conflict. It runs in your browser.

Two other things I reach for while building against this API. Ephemeral and rich message payloads nest several levels deep, and a 400 Bad Request from Telegram tells you almost nothing about which brace you missed, so run the body through the JSON Formatter before you blame the API. And if the bot wraps a model, the API Cost Calculator is how you find out what streaming a rich message per user actually costs, which matters more once you are paying per token for output nobody keeps.

What to actually do this week

Ordered by how likely each one is to bite you:

  1. Grep for message_id used as truthy or as a key. Fix the 0 case.
  2. If you shipped ephemeral support in July, migrate to ephemeral_message_parameters.
  3. Mark your per-user commands is_ephemeral and watch group noise drop.
  4. Decide whether your bot needs admin rights to escape the 15-second window.
  5. Only then look at rich messages. They are the biggest change and the least urgent.

Rate limits have not changed: 30 messages per second by default, up to 1000 with Paid Broadcasts enabled in @BotFather, at 0.1 Stars per message over the free amount, and you need at least 10,000 Stars on the bot's balance to turn it on. Successful broadcasts are the only ones charged.

FAQ

What is the latest Telegram Bot API version?

Bot API 10.3, released August 24, 2026. It added EphemeralMessageParameters, RichMessageButton, expandable block quotations, disabled inline keyboard buttons, and can_send_welcome_messages on admin rights.

Can other members see an ephemeral message?

No. Telegram's documentation states that other members of the group or supergroup will not see it. Only the user in receiver_user_id and the bot do.

Why did my ephemeral message fail to send?

Most likely you were outside the 15-second window without being a chat administrator, or you sent no callback_query_id and no reply_parameters.ephemeral_message_id. Delivery is also not guaranteed when the user is offline, so a silent non-delivery is documented behaviour rather than a bug.

Do I need to change code written for Bot API 10.2?

Yes, if you used ephemeral messages. The top-level receiver_user_id and callback_query_id parameters were replaced by the ephemeral_message_parameters object in 10.3.

Are rich message drafts saved in the chat?

No. A draft is a 30-second preview. Call sendRichMessage with the finished message to persist it.

How do I get a Telegram group or channel ID?

Add your bot to the chat, send a message there, then read the chat ID from getUpdates, or use the Telegram Chat ID Finder which does the token validation and -100 handling for you. Remember that getUpdates returns nothing while a webhook is set.

Conclusion

Ephemeral messages are the change I would ship first. The rest of 10.2 and 10.3 is additive, but this one removes an entire category of workaround: bots that DM users who never opened a private chat, bots that edit a shared message and confuse 4,000 people, bots that go quiet in groups because any reply is too noisy.

Two footguns to respect. message_id is 0 for ephemeral messages, and the parameter shape changed between two releases six weeks apart. Both are the kind of thing you find at 2am rather than in code review.

Sources

Tools In This Article

Browser-based, no sign-up. Try them while the topic is fresh.

More From ToolMintX

Other Blog Posts