Appearance
Inline mode
Inline mode lets a bot be used from any chat without being a member of it. The user types @your_bot followed by a query in the composer, results appear above the keyboard, and picking one sends that message into the chat — attributed to the user, marked via @your_bot.
It is the only way a bot's output reaches a conversation it was never added to.
Not implemented
No inline query will reach your bot today: the gateway route is live, but no token can be issued, so no method can be called. This page documents the specified flow. Implementation status.
The flow
- The user types
@your_bot weather tashin any chat. The client debounces briefly rather than querying per keystroke. - The bot receives an
inline_queryupdate — the query text, the sender, anoffsetfor paging, and optionally a location. - The bot replies with
answerInlineQuery, returning up to 50 results. - The user picks one. The client sends it into the chat as a message from the user, with
via_botfilled in. - If inline feedback is enabled for the bot, a
chosen_inline_resultupdate follows.
Inline mode must be enabled for the bot before any of this happens; a bot that has not enabled it is simply not offered when the user types its username. Location-bearing queries require a further opt-in, and without it the location field never arrives no matter what the client would have sent.
json
{
"update_id": 100077,
"inline_query": {
"id": "1030329944103343201",
"from": {"id": 1337, "is_bot": false, "first_name": "Aziz"},
"query": "tash",
"offset": ""
}
}Answering
bash
curl "https://api.casinoplaneta.info/bot$GROM_TOKEN/answerInlineQuery" \
-H 'Content-Type: application/json' \
-d '{
"inline_query_id": "1030329944103343201",
"results": [
{
"type": "article",
"id": "city-tashkent",
"title": "Tashkent",
"description": "Clear, 31 °C",
"input_message_content": {
"message_text": "*Tashkent* — clear, 31 °C, wind 3 m/s",
"parse_mode": "MarkdownV2"
}
}
],
"cache_time": 60,
"is_personal": true
}'Result ids must be unique within the response, and the one you choose comes back in chosen_inline_result. Store your own key there — an array index tells you nothing once the result set has moved on.
Results come in a long list of types: article, photo, gif, mpeg4_gif, video, audio, voice, document, location, venue, contact, game, sticker, plus cached_* variants that re-send media the platform already holds by file_id. Clients render them either as a vertical list or as a gallery grid.
Failure is silent
An unanswered inline query produces nothing on the user's screen. No error, no spinner, no retry — the results area stays empty and looks identical to "this bot has no matches for that". A broken inline handler can run for weeks without a single complaint that identifies it.
There is no way to detect this from the outside. Log every query you fail to answer and alert on the ratio; that instrumentation is the only signal you get.
The window is also short — the user is waiting on it. Answer with whatever you have rather than blocking on a slow upstream: an empty result set delivered promptly is a better failure than a timely one that arrives too late to be shown.
Cache and privacy, decided together
cache_time and is_personal are one decision, not two, and getting it wrong is how inline bots leak data.
The server caches an answer against the query text. If the results depended on who was asking — their account, their permissions, their own records — and is_personal is not set, the set computed for the first user is served to everyone else who types the same words.
This is a data-leak shape, not a performance bug
Any result derived from the requesting user needs is_personal: true. Without it the cache is shared across users keyed only on the query string, and one person's private results are handed to the next person who types the same thing.
The cache lives server-side. Lowering cache_time afterwards does not invalidate what is already stored, so the fix does not retroactively clean up a leak — it only stops new ones.
cache_time defaults to 300 seconds, which is far too long for anything live. Prices, availability, and anything a user just changed need a short cache or none at all.
Paging
Send next_offset and the client requests more when the user scrolls to the end; the value comes back as offset on the follow-up query. The offset is an opaque string of your choosing — an index, a cursor, a timestamp.
Two rules, and both are easy to break:
- Omit it, or send an empty string, to signal the end. Anything else means "there is more".
- Never echo back the offset you were given. The client will page forever, requesting the same set indefinitely, and the user sees an infinitely scrolling list of duplicates.
python
offset = int(q["offset"] or 0)
page = search(q["query"])[offset:offset + 20]
requests.post(f"{API}/answerInlineQuery", json={
"inline_query_id": q["id"],
"results": [to_result(r) for r in page],
# empty string when the page came back short — never str(offset)
"next_offset": str(offset + 20) if len(page) == 20 else "",
"is_personal": True,
"cache_time": 0,
})The button above the results
answerInlineQuery accepts a button rendered above the result list. Its usual job is authentication: a bot that cannot produce results until it knows who the user is shows a button that sends them into a private chat with it, carrying a start parameter, instead of returning an empty list with no explanation.
The same button can open a Mini App rather than a chat.
The start parameter follows the deep-link rules — 1–64 characters of [A-Za-z0-9_-] — so it is a key you look up, not a payload you pack. See also /start handling.
Editing what you cannot see
A message sent through inline mode does not belong to a chat the bot can address. There is no chat_id, no message_id; the bot never learns where it landed. What it gets instead is an inline_message_id in chosen_inline_result and in any callback_query from buttons on that message.
Pass that id to editMessageText in place of the chat and message pair. It is the only handle you will ever have on that message, so persist it at the moment it arrives if you intend to update the message later.
Whether chosen_inline_result is delivered for every pick or only when feedback is enabled — and whether delivery is sampled at all — is not settled in the specification. Do not build billing, analytics or state transitions that assume you receive one for every result a user sends.
Prepared inline messages
A Mini App cannot send a message into a chat by itself. The prepared-message flow lets it offer one to the user, who then chooses where it goes:
- The bot calls
savePreparedInlineMessagewith auser_idand a result object, and receives anidand an expiration. - The Mini App passes that id to
shareMessage, which opens the chat picker. - The user picks a chat and confirms; the message is sent.
- The Mini App is notified of the outcome — sent, declined by the user, or failed.
json
{
"user_id": 41028837,
"result": {
"type": "article",
"id": "invite-9f21c0",
"title": "Invite to my team",
"input_message_content": {
"message_text": "Join my team: https://code.casinoplaneta.info/team_bot?start=9f21c0"
}
},
"allow_user_chats": true,
"allow_group_chats": true
}Three things to get right:
- Set at least one
allow_*flag. With all four false the picker has nowhere to send the message. The call itself succeeds; the failure only surfaces inside the Mini App, which makes it a confusing bug to trace back to a missing boolean. - Prepared messages expire. Read
expiration_datefrom the result. Prepare at the moment the user taps share, not during page load — a message prepared on load may be dead by the time they decide. - The id is bound to one account. Preparing for one user and handing the id to another does not work, so it is not a general-purpose share link.
A declined share is a normal outcome, not an error. Treat it as such in the Mini App's UI rather than showing a failure message to someone who simply changed their mind.
See also
- Buttons and keyboards —
switch_inline_querybuttons that launch an inline query for the user - Mini Apps — where prepared messages come from
answerInlineQuery· Errors · Limits