Building UI

Inline mode

Inline mode lets users invoke your bot from any chat by typing @yourbot followed by a query. The bot returns a list of clickable results that the user can pick from.

What inline mode is

Telegram clients show a popup of results next to the user's text input as they type @yourbot query. The bot sees every keystroke as a fresh inline query, returns up to 50 results, and the chosen result is posted as a regular message authored by the user but marked "via @yourbot".

Enabling it in BotFather

Inline mode is opt-in per bot. Talk to BotFather: /setinline→ pick the bot → supply a placeholder string (shown when the user types just @yourbot). To receive chosen_inline_result feedback events, also run /setinlinefeedback.

Handling queries

Every incoming InlineQuery carries the query text, a QueryID, the sender, an Offset string for pagination, and a PeerType field so you can adapt results by chat kind (private, group, channel).

The high-level path uses q.Builder(), chains result helpers, and calls Answer:

client.On("inline:*", func(q *telegram.InlineQuery) error {
	_, err := q.Builder().
		Article("Upper", "Uppercase the query", strings.ToUpper(q.Query)).
		Article("Lower", "Lowercase the query", strings.ToLower(q.Query)).
		CacheTime(60).
		IsPersonal(true).
		Answer()
	return err
})
client.On("inline:*", func(q *telegram.InlineQuery) error {
	_, err := q.Builder().
		Article("Upper", "Uppercase the query", strings.ToUpper(q.Query)).
		Article("Lower", "Lowercase the query", strings.ToLower(q.Query)).
		CacheTime(60).
		IsPersonal(true).
		Answer()
	return err
})

The InlineBuilder

q.Builder() returns an *InlineBuilder tied to the query id. Add results with the shorthand helpers, tune options fluently, then submit with Answer(). The whole chain is a single expression — no manual result struct construction unless you want it.

Result helpers:

Article(title, description, text, opts...)
The most common one. Displays as a two-line preview; picking it sends text. Extras go on ArticleOptions.
Photo(photo, opts...)
Reuses an existing InputPhoto or accepts any of the media-input forms. Great when you already have the photo uploaded.
Document(doc, opts...)
Videos, animations, files. Preview thumbnail comes from the document itself.
Game(id, shortName, opts...)
An HTML5 game registered with BotFather. Requires a matching Button.Game in the markup.
Media(media, opts...)
Generic fallback when you have an already-built media object and know its type.

Builder-wide tuners:

MaxResults(n)
Cap the number of results after appending. Cheap way to avoid busting the 50-result limit if your search returns more.
CacheTime(seconds)
How long the client caches these results for the same query. Default 60.
IsPersonal(bool)
Restrict caching to the requesting user. Set true when results depend on who is asking.
NextOffset(token)
Pagination cursor. Telegram passes it back as q.Offset when the user scrolls.
SwitchPM(text, startParam)
Banner above the result list that opens a private chat with the bot on tap.
WithReplyMarkup / WithLinkPreview / WithThumb / WithContent
Modify the last-added result. Handy when the result-helper defaults don't match what you need.

Longer example combining several of these:

client.On("inline:search", func(q *telegram.InlineQuery) error {
	b := q.Builder()

	for _, hit := range search(q.Query) {
		b.Article(hit.Title, hit.Snippet, hit.URL, &telegram.ArticleOptions{
			Description: hit.Snippet,
			Thumb:       telegram.InputWebDocument{URL: hit.Thumb},
			LinkPreview: true,
			ParseMode:   "HTML",
		})
	}

	if b.Error() != nil {
		return b.Error()
	}

	_, err := b.
		MaxResults(50).
		CacheTime(300).
		NextOffset(next).
		Answer()
	return err
})
client.On("inline:search", func(q *telegram.InlineQuery) error {
	b := q.Builder()

	for _, hit := range search(q.Query) {
		b.Article(hit.Title, hit.Snippet, hit.URL, &telegram.ArticleOptions{
			Description: hit.Snippet,
			Thumb:       telegram.InputWebDocument{URL: hit.Thumb},
			LinkPreview: true,
			ParseMode:   "HTML",
		})
	}

	if b.Error() != nil {
		return b.Error()
	}

	_, err := b.
		MaxResults(50).
		CacheTime(300).
		NextOffset(next).
		Answer()
	return err
})

b.Error()is a sticky error accumulator on the builder — a helper that fails (bad photo id, unresolvable inline user) records the error and later calls become no-ops. Check it before Answer so the failure surfaces.

ArticleOptions

The catch-all option bag passed to every result helper. Not every field applies to every result kind — the builder ignores fields that don't fit. Highlights:

ID string
Unique result id. Must be unique within the answer. Auto-generated if you omit it.
Title / Description string
Shown in the preview list. Description overrides the default that some helpers derive from the content.
Thumb InputWebDocument
Small preview image URL for article results. Use WithThumbURL as a shortcut when you only have a URL.
Content InputWebDocument
Full-size media URL for article results with attached media (audio, video linked from a page).
MimeType string
Override the MIME hint on document results.
ForceDocument bool
Send as an attachment instead of an inline embed.
Caption string
Caption for media results (photo, document, video).
ParseMode string
"HTML" or "Markdown" for the sent message body.
Entities []MessageEntity
Pre-parsed entity list, wins over ParseMode when set.
LinkPreview bool
Enable link unfurl in the resulting message.
InvertMedia bool
Show media below the caption instead of above.
ExcludeMedia bool
Keep the media out of the sent message and only ship the text.
ReplyMarkup ReplyMarkup
Inline keyboard attached to the resulting message.
Venue / Location / Contact / Invoice / WebPage
Turn the result into a venue, location, contact card, invoice, or web page preview instead of a plain message.
BusinessConnectionId string
Answer on behalf of a business connection.

Photo, document, and game results

b.Photo(photo, &telegram.ArticleOptions{
	Caption:   "click to send",
	ParseMode: "HTML",
})
b.Photo(photo, &telegram.ArticleOptions{
	Caption:   "click to send",
	ParseMode: "HTML",
})
b.Document(document, &telegram.ArticleOptions{
	Title:       "report.pdf",
	Description: "Q3 numbers",
	MimeType:    "application/pdf",
})
b.Document(document, &telegram.ArticleOptions{
	Title:       "report.pdf",
	Description: "Q3 numbers",
	MimeType:    "application/pdf",
})
b.Game("game-id", "shortname", &telegram.ArticleOptions{
	ReplyMarkup: telegram.NewKeyboard().AddRow(
		telegram.Button.Game("Play"),
	).Build(),
})
b.Game("game-id", "shortname", &telegram.ArticleOptions{
	ReplyMarkup: telegram.NewKeyboard().AddRow(
		telegram.Button.Game("Play"),
	).Build(),
})

Pagination and caching

Each answer holds up to 50 results. For infinite scroll, set NextOffset to a token you can decode later; the client passes it back as q.Offset when the user scrolls past the end.

CacheTimeis a hint to the client. High values (600–3600 seconds) are fine for static content, low values (5–30) for personalised results. Set IsPersonal(true) when results depend on the sender so the cache does not leak across users.

Raw path when you need something the builder doesn't model:

_, err := q.Answer(
	[]telegram.InputBotInlineResult{
		&telegram.InputBotInlineResultObj{
			ID:    "1",
			Type:  "article",
			Title: "Hand-built",
			SendMessage: &telegram.InputBotInlineMessageText{
				Message: "you picked the hand-built result",
			},
		},
	},
	&telegram.InlineSendOptions{CacheTime: 60},
)
_, err := q.Answer(
	[]telegram.InputBotInlineResult{
		&telegram.InputBotInlineResultObj{
			ID:    "1",
			Type:  "article",
			Title: "Hand-built",
			SendMessage: &telegram.InputBotInlineMessageText{
				Message: "you picked the hand-built result",
			},
		},
	},
	&telegram.InlineSendOptions{CacheTime: 60},
)

Switch-to-PM banner

For bots that need setup or login before answering, show a banner above the results list that opens a private chat with the bot on tap:

_, err := q.Builder().
	Article("Sign in first", "Tap to authorise", "").
	SwitchPM("Sign in to enable suggestions", "from_inline").
	Answer()
_, err := q.Builder().
	Article("Sign in first", "Tap to authorise", "").
	SwitchPM("Sign in to enable suggestions", "from_inline").
	Answer()

Tapping the banner opens the DM and starts /start from_inline, where the second argument is the startParam you passed. Use it to thread state through the start payload.