Telegram Inline Keyboard Builder
Lay the buttons out the way your users will see them and copy the reply_markup JSON straight into your sendMessage call. callback_data is counted in bytes as you type, which is the limit that actually bites. No token, nothing sent anywhere.
Build your keyboard
{
"reply_markup": {
"inline_keyboard": [
[
{
"text": "Pricing",
"callback_data": "menu:pricing"
},
{
"text": "Support",
"callback_data": "menu:support"
}
],
[
{
"text": "Open docs",
"url": "https://core.telegram.org/bots/api"
}
]
]
}
}How an inline keyboard is structured
reply_markup takes an InlineKeyboardMarkup, which holds a single field: inline_keyboard. That field is an array of rows, and each row is an array of buttons. Two nested arrays, no more than that - the commonest mistake is flattening it to one, which Telegram rejects.
Every button needs visible text, plus exactly one field that says what pressing it does. A button with both url and callback_data does not fall back to one of them - it fails the whole send.
The keyboard belongs to the message it was sent with, so editing that message later can swap the buttons out. That is how paging, wizards and confirm-then-collapse flows are built.
The 64-byte limit that catches everyone
callback_data is limited to 64 bytes, not 64 characters. That distinction is the whole problem. ASCII costs one byte per character, but an accented Latin letter costs two, most CJK characters cost three, and the majority of emoji cost four.
| callback_data | Characters | Bytes | Accepted |
|---|---|---|---|
| order:1234:confirm | 18 | 18 | Yes |
| reserva:cafetería:confirmar | 27 | 29 | Yes |
| 20 emoji in a row | 20 | 80 | No |
The practical fix is to keep callback_data as a short opaque key - o:1234:c - and hold the human-readable version in your own store. It stays under the limit whatever language your users speak, and it keeps internal identifiers out of a field the client can read.
Button types, and when each one fits
Each button carries one action field. These are the four worth knowing for ordinary bots; the API also defines login_url, callback_game, copy_text and pay, which carry extra placement rules.
| Field | What pressing it does | Use it for |
|---|---|---|
| callback_data | Sends the value back to your bot as a callback query | Menus, confirmations, anything the bot must react to |
| url | Opens a link outside the chat | Docs, checkout pages, anything already on the web |
| web_app | Opens a Mini App inside Telegram (https only) | Forms and flows too rich for chat |
| switch_inline_query | Opens a chat picker with a prefilled query | Sharing, and inviting the user to post your bot elsewhere |
Placement rules. A pay button must be the first button of the first row, and the same holds for login_url and callback_game. Anywhere else and the send fails.
Inline keyboard or reply keyboard?
They are different objects and picking the wrong one is a design mistake rather than a syntax error. An inline keyboard attaches to one message and leaves the user's typing keyboard alone. A reply keyboard replaces the on-screen keyboard for the whole chat, and its buttons send plain text as if the user had typed it.
Use inline for anything tied to a moment: confirm this order, page through these results, pick a slot. Use a reply keyboard for a persistent main menu in a bot where typing is rare. If you are unsure, inline is the safer default because it never takes the keyboard away from someone mid-sentence.
Building the flow rather than the JSON? A Telegram chatbot built in a visual editor handles the buttons, the callback routing and the state for you, and the same flow deploys to your other channels unchanged.
What this tool does with your data
Nothing. The JSON is assembled in your browser from what you type. No bot token is requested, no request is made to Telegram or to us, and nothing is stored. There is no account, no email gate and no usage limit.
If you are debugging a bot that is not responding at all, the token checker calls getMe and getWebhookInfo for you, the Telegram error directory documents each code, and Telegram API limits covers the rate and size caps. Background on webhooks is in the glossary.
Telegram inline keyboard questions
What is a Telegram inline keyboard?
It is a grid of buttons attached underneath a bot message, sent as the reply_markup parameter. Unlike a reply keyboard, it does not replace the user's typing keyboard and it stays with the message it belongs to. Buttons can send data back to your bot, open a link, or launch a Mini App, and the message can be edited afterwards to change the buttons.
Why does Telegram reject my callback_data?
Almost always because it exceeds 64 bytes. The limit is measured in bytes rather than characters, so any non-ASCII text costs more than it looks: an accented letter is two bytes and most emoji are four. A label that reads as 30 characters can easily be 70 bytes. This builder counts the bytes for you and flags a button before you ship it.
Can one button both open a URL and send callback data?
No. An InlineKeyboardButton carries its text plus exactly one optional field. Setting url and callback_data on the same button makes the API reject the whole message, not just that button. If you need both behaviours, use two buttons - which is usually clearer for the user anyway.
What is the difference between an inline keyboard and a reply keyboard?
An inline keyboard is attached to a specific message and uses InlineKeyboardMarkup. A reply keyboard replaces the user's on-screen keyboard for the whole chat and uses ReplyKeyboardMarkup. They are separate objects and are not interchangeable. Inline is the right choice for anything tied to one message, such as confirming an order or paging through results.
How many buttons can I put in a row?
Telegram does not publish a hard cap, but each row renders across the width of the chat, so labels are truncated as you add more. Two or three buttons per row stays readable on a phone, which is where most people will see it. Long labels deserve a row of their own rather than being squeezed alongside another button.
Do pay buttons have special rules?
Yes. A pay button must be the first button of the first row, and the same restriction applies to login_url and callback_game buttons. Placing any of them elsewhere in the grid causes the send to fail. If a keyboard mixes a payment button with ordinary ones, the payment button has to lead.
Is my data sent anywhere by this tool?
No. The JSON is assembled in your browser from what you type, and nothing leaves the page. No bot token is requested, no request is made to Telegram, and nothing is stored. You can disconnect from the network after the page loads and the builder still works exactly the same.