Buttons & keyboards
Every interactive element a bot can attach to a message. Inline keyboards, reply keyboards, ForceReply, request-data buttons — built fluently with NewKeyboard().AddRow(...) and the Button helpers.
Two keyboard systems
Inline keyboardsattach under a specific message. Tapping a button triggers a callback, opens a URL, launches a WebView, or starts a payment flow — the button itself stays on the message and you can edit it any time. Use these for actions tied to content.
Reply keyboardsreplace the user's on-screen keyboard with a panel of buttons. Tapping one sends its label as a regular text message; there is no callback. Useful for menu-style bots and asking for structured input like a contact or location.
The builder
Start with telegram.NewKeyboard() and chain AddRow(buttons...) for each row. Finish with Build() for an inline keyboard or BuildReply(opts) for a reply keyboard. Buttons come from telegram.Button, a value of type ButtonBuilderexposed at package level — it is just a namespace.
Other handy builders: NewGrid(x, y, buttons...) packs buttons into a grid, NewRow(perRow, buttons...) auto-wraps after N per row, NewColumn(perCol, buttons...) wraps vertically.
Data (callback) buttons
kbd := telegram.NewKeyboard().
AddRow(
telegram.Button.Data("👍 Like", "like:" + strconv.Itoa(int(postID))),
telegram.Button.Data("👎 Dislike", "dislike:" + strconv.Itoa(int(postID))),
).
Build()
client.SendMessage(peerID, "Rate this post:", &telegram.SendOptions{
ReplyMarkup: kbd,
})kbd := telegram.NewKeyboard().
AddRow(
telegram.Button.Data("👍 Like", "like:" + strconv.Itoa(int(postID))),
telegram.Button.Data("👎 Dislike", "dislike:" + strconv.Itoa(int(postID))),
).
Build()
client.SendMessage(peerID, "Rate this post:", &telegram.SendOptions{
ReplyMarkup: kbd,
})The data string is an arbitrary payload up to 64 bytes that comes back to you as a callback query when the user taps the button. Convention is a colon-separated verband arguments (like:42, page:next:5), but anything is allowed. See Callback queries for handling.
URL buttons
kbd := telegram.NewKeyboard().
AddRow(
telegram.Button.URL("Read the docs", "https://gogram.dev"),
telegram.Button.URL("Source", "https://github.com/AmarnathCJD/gogram"),
).
Build()kbd := telegram.NewKeyboard().
AddRow(
telegram.Button.URL("Read the docs", "https://gogram.dev"),
telegram.Button.URL("Source", "https://github.com/AmarnathCJD/gogram"),
).
Build()Open the URL in the user's browser. No callback. The link gets an unfurl preview like any link in a regular message.
WebView buttons
kbd := telegram.NewKeyboard().
AddRow(
telegram.Button.WebView("🛒 Open shop", "https://shop.example.com"),
).
Build()kbd := telegram.NewKeyboard().
AddRow(
telegram.Button.WebView("🛒 Open shop", "https://shop.example.com"),
).
Build()Open the URL inside the in-app browser with the Telegram Web App JavaScript bridge attached, so the page can call Telegram.WebApp.sendData(...) and the bot receives a messageActionWebViewDataSentMe update.
SimpleWebView is the same idea without the data-callback contract: a regular web page that just happens to know it is inside Telegram. Good for status pages, dashboards, anything read-only.
kbd := telegram.NewKeyboard().
AddRow(
telegram.Button.SimpleWebView("Order status", "https://shop.example.com/order"),
).
Build()kbd := telegram.NewKeyboard().
AddRow(
telegram.Button.SimpleWebView("Order status", "https://shop.example.com/order"),
).
Build()Web Apps in groups
The same trick works for inline-keyboard menu buttons on the bot's profile: configure the menu button as a Web App in BotFather, and any t.me/<bot>/<app>link from inside a group, channel, or even another bot opens the Web App correctly.
SwitchInline
kbd := telegram.NewKeyboard().
AddRow(
telegram.Button.SwitchInline("Search this chat", true, "query"),
telegram.Button.SwitchInline("Search elsewhere", false, "query"),
).
Build()kbd := telegram.NewKeyboard().
AddRow(
telegram.Button.SwitchInline("Search this chat", true, "query"),
telegram.Button.SwitchInline("Search elsewhere", false, "query"),
).
Build()Pre-fills the user's input box with @yourbot <query>. The samePeer flag picks between the current chat (true) and a chat the user is asked to select (false). Handy when your bot does most of its work as an inline result and you want to nudge the user there from a regular message.
Auth (login) buttons
// Auth(text, url, forwardText, bot, requestWriteAccess...)
bot, _ := client.ResolvePeer("@my_dashboard_bot")
inputBot := bot.(*telegram.InputPeerUser)
kbd := telegram.NewKeyboard().
AddRow(
telegram.Button.Auth(
"Sign in to dashboard",
"https://app.example.com/auth/telegram",
"Open dashboard",
&telegram.InputUserObj{UserID: inputBot.UserID, AccessHash: inputBot.AccessHash},
true,
),
).
Build()// Auth(text, url, forwardText, bot, requestWriteAccess...)
bot, _ := client.ResolvePeer("@my_dashboard_bot")
inputBot := bot.(*telegram.InputPeerUser)
kbd := telegram.NewKeyboard().
AddRow(
telegram.Button.Auth(
"Sign in to dashboard",
"https://app.example.com/auth/telegram",
"Open dashboard",
&telegram.InputUserObj{UserID: inputBot.UserID, AccessHash: inputBot.AccessHash},
true,
),
).
Build()Opens an authorization URL with a signed payload your server can verify against Telegram's public key. The classic use is "sign in to our web dashboard with your Telegram account" without rolling a separate OAuth provider. The forwardText argument is the alternative label shown when forwarding the message; requestWriteAccess asks for permission to send the user a message as the bot once login completes.
Buy buttons
// Invoice messages get a Pay button attached automatically by the
// server, so you rarely need to build one yourself. To customise the
// label of an in-message Buy button:
kbd := telegram.NewKeyboard().
AddRow(telegram.Button.Buy("⭐ Subscribe — $9.99")).
Build()// Invoice messages get a Pay button attached automatically by the
// server, so you rarely need to build one yourself. To customise the
// label of an in-message Buy button:
kbd := telegram.NewKeyboard().
AddRow(telegram.Button.Buy("⭐ Subscribe — $9.99")).
Build()Payments use a separate invoice flow. When you send an invoice, Telegram automatically attaches a Pay button. You rarely need to build one yourself; Button.Buyis for when you want to customise the label on a follow-up message.
Game buttons
kbd := telegram.NewKeyboard().
AddRow(telegram.Button.Game("🎮 Play")).
Build()kbd := telegram.NewKeyboard().
AddRow(telegram.Button.Game("🎮 Play")).
Build()Launches an HTML5 game registered with BotFather. Attach exactly one Button.Game to a game message; tapping it opens the game URL with the Telegram games bridge.
Copy buttons
kbd := telegram.NewKeyboard().
AddRow(telegram.Button.Copy("📋 Copy receipt id", receiptID)).
Build()kbd := telegram.NewKeyboard().
AddRow(telegram.Button.Copy("📋 Copy receipt id", receiptID)).
Build()Copies the given string to the user's clipboard on tap. No network call, no callback. Good for coupon codes, transaction ids, share links.
User mention buttons
target, _ := client.ResolvePeer(userID)
inputUser := target.(*telegram.InputPeerUser)
kbd := telegram.NewKeyboard().
AddRow(
telegram.Button.Mention(
"Open profile",
&telegram.InputUserObj{
UserID: inputUser.UserID,
AccessHash: inputUser.AccessHash,
},
),
).
Build()target, _ := client.ResolvePeer(userID)
inputUser := target.(*telegram.InputPeerUser)
kbd := telegram.NewKeyboard().
AddRow(
telegram.Button.Mention(
"Open profile",
&telegram.InputUserObj{
UserID: inputUser.UserID,
AccessHash: inputUser.AccessHash,
},
),
).
Build()Renders an inline mention badge that opens the target user's profile when tapped. Takes an InputUser; for users you have not seen in this chat, resolve via client.ResolvePeer first.
Reply keyboards
kbd := telegram.NewKeyboard().
AddRow(
telegram.Button.Text("📊 Stats"),
telegram.Button.Text("🛠 Settings"),
).
AddRow(
telegram.Button.Text("❓ Help"),
).
BuildReply(telegram.BuildReplyOptions{
ResizeKeyboard: true,
OneTime: false,
Placeholder: "pick one",
})
client.SendMessage(peerID, "Menu:", &telegram.SendOptions{ReplyMarkup: kbd})kbd := telegram.NewKeyboard().
AddRow(
telegram.Button.Text("📊 Stats"),
telegram.Button.Text("🛠 Settings"),
).
AddRow(
telegram.Button.Text("❓ Help"),
).
BuildReply(telegram.BuildReplyOptions{
ResizeKeyboard: true,
OneTime: false,
Placeholder: "pick one",
})
client.SendMessage(peerID, "Menu:", &telegram.SendOptions{ReplyMarkup: kbd})Reply keyboards use Button.Text(label) for plain buttons. The label is sent back to the bot as a regular message when tapped. Customise with BuildReplyOptions:
- ResizeKeyboard
- Shrink the keyboard vertically to fit the buttons.
- OneTime
- Hide the keyboard after a single use.
- Placeholder
- Placeholder shown inside the message input box while the keyboard is up.
- Selective
- In a group, show the keyboard only to mentioned users or the reply target.
- Persistent
- Keep the keyboard visible even after the user sends a message that closes it.
Request buttons
kbd := telegram.NewKeyboard().
AddRow(telegram.Button.RequestPhone("📱 Share contact")).
AddRow(telegram.Button.RequestLocation("📍 Share location")).
AddRow(telegram.Button.RequestPoll("📊 Start a poll", false)).
AddRow(telegram.Button.RequestPeer("👤 Pick a user", 1, telegram.RequestPeerType{
// constrain by peer type as needed
})).
BuildReply(telegram.BuildReplyOptions{ResizeKeyboard: true})kbd := telegram.NewKeyboard().
AddRow(telegram.Button.RequestPhone("📱 Share contact")).
AddRow(telegram.Button.RequestLocation("📍 Share location")).
AddRow(telegram.Button.RequestPoll("📊 Start a poll", false)).
AddRow(telegram.Button.RequestPeer("👤 Pick a user", 1, telegram.RequestPeerType{
// constrain by peer type as needed
})).
BuildReply(telegram.BuildReplyOptions{ResizeKeyboard: true})Reply keyboards can ask the user for structured data instead of plain text:
- RequestPhone
- The user shares their phone number / contact card. Note: it is
RequestPhonenotRequestContact. - RequestLocation
- The user shares a one-shot geo point.
- RequestPoll(text, isQuiz)
- Opens the poll composer; pass
truefor a quiz. - RequestPeer(text, buttonID, peerType)
- The user picks a user, bot, group, or channel from a chooser. The
buttonIDties the eventual choice back to this button;peerTypeconstrains what the user can pick.
ForceReply
kbd := telegram.Button.Force("you@example.com")
client.SendMessage(peerID, "Reply with your email:", &telegram.SendOptions{
ReplyMarkup: kbd,
})kbd := telegram.Button.Force("you@example.com")
client.SendMessage(peerID, "Reply with your email:", &telegram.SendOptions{
ReplyMarkup: kbd,
})Button.Force(placeholder)returns a markup that forces the user's next message in this chat to start as a reply to this one. Useful for one-off Q&A without setting up a full conversation.
Removing keyboards
// Remove the active reply keyboard:
client.SendMessage(peerID, "Thanks!", &telegram.SendOptions{
ReplyMarkup: telegram.Button.Clear(),
})
// Remove an inline keyboard from an existing message by editing
// with an empty markup:
client.EditMessage(peerID, msgID, m.Text(), &telegram.SendOptions{
ReplyMarkup: telegram.NewKeyboard().Build(),
})// Remove the active reply keyboard:
client.SendMessage(peerID, "Thanks!", &telegram.SendOptions{
ReplyMarkup: telegram.Button.Clear(),
})
// Remove an inline keyboard from an existing message by editing
// with an empty markup:
client.EditMessage(peerID, msgID, m.Text(), &telegram.SendOptions{
ReplyMarkup: telegram.NewKeyboard().Build(),
})Button.Clear()returns a hide-keyboard markup — attach it to any send to take down an active reply keyboard. To clear inline buttons from a previously-sent message, edit the message with an empty NewKeyboard().Build().
