BotNetwork

Teclados inline y callback queries en Telegram, con código que funciona

Los teclados inline convierten un bot de texto en una pequeña app: menús, confirmaciones, paginación. Son sencillos cuando conoces las tres reglas de donde salen casi todos los errores.

Publicado 4 min de lectura

Dos tipos de teclado

Telegram tiene dos tipos de teclado y se comportan distinto:

  • Teclados de respuesta: reemplazan el teclado del usuario con botones que envían su etiqueta como mensaje normal.
  • Teclados inline: van pegados debajo de un mensaje. Pulsar un botón no envía un mensaje; le manda a tu bot una callback query (o abre una URL, un inicio de sesión o una Mini App).

Este artículo trata los teclados inline. Puedes diseñarlos visualmente en nuestro constructor de teclados de Telegram y copiar el JSON.

La estructura

Un teclado inline es un arreglo de filas; cada fila es un arreglo de botones:

{
  "inline_keyboard": [
    [ { "text": "Sí", "callback_data": "confirm:yes:42" }, { "text": "No", "callback_data": "confirm:no:42" } ],
    [ { "text": "Leer la documentación", "url": "https://core.telegram.org/bots/api" } ]
  ]
}

Cada botón necesita exactamente un campo de acción, como callback_data, url, switch_inline_query o web_app.

Regla 1: callback_data tiene máximo 64 bytes

callback_data está limitado a 1-64 bytes, y son bytes, no caracteres: las letras con acento y otros caracteres no ASCII ocupan más de un byte. No metas objetos JSON. Usa un formato corto y fácil de leer como accion:arg:id, o guarda los datos en el servidor y envía solo una clave.

Regla 2: responde siempre la callback query

Cuando el usuario pulsa un botón, el cliente de Telegram muestra un indicador de carga hasta que tu bot llama a answerCallbackQuery. Llámalo aunque no tengas nada que decir; opcionalmente pasa text para un aviso breve o show_alert: true para un diálogo.

Regla 3: edita, no llenes el chat

Para menús y paginación, cambia el mensaje existente con editMessageText o editMessageReplyMarkup en lugar de enviar uno nuevo. Si el texto y el teclado nuevos son idénticos a los actuales, Telegram devuelve el error "message is not modified". Compara antes de editar o captura e ignora ese error específico.

Ejemplo con grammY

import { Bot, InlineKeyboard } from "grammy";

const bot = new Bot(process.env.BOT_TOKEN!);

bot.command("borrar", async (ctx) => {
  const kb = new InlineKeyboard()
    .text("Sí, borrar", "del:yes:42").text("Cancelar", "del:no:42");
  await ctx.reply("¿Borrar el elemento 42?", { reply_markup: kb });
});

bot.callbackQuery(/^del:(yes|no):(\d+)$/, async (ctx) => {
  const [, choice, id] = ctx.match!;
  await ctx.answerCallbackQuery(choice === "yes" ? "Borrado" : "Cancelado");
  await ctx.editMessageText(choice === "yes" ? `Elemento ${id} borrado.` : "No cambió nada.");
});

// respuesta genérica para que ninguna query quede sin contestar
bot.on("callback_query:data", (ctx) => ctx.answerCallbackQuery());

bot.start();

Ejemplo con python-telegram-bot

import os
from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update
from telegram.ext import Application, CallbackQueryHandler, CommandHandler, ContextTypes

async def borrar(update: Update, context: ContextTypes.DEFAULT_TYPE):
    kb = InlineKeyboardMarkup([[
        InlineKeyboardButton("Sí, borrar", callback_data="del:yes:42"),
        InlineKeyboardButton("Cancelar", callback_data="del:no:42"),
    ]])
    await update.message.reply_text("¿Borrar el elemento 42?", reply_markup=kb)

async def on_borrar(update: Update, context: ContextTypes.DEFAULT_TYPE):
    q = update.callback_query
    _, choice, item = q.data.split(":")
    await q.answer("Borrado" if choice == "yes" else "Cancelado")
    await q.edit_message_text(f"Elemento {item} borrado." if choice == "yes" else "No cambió nada.")

app = Application.builder().token(os.environ["BOT_TOKEN"]).build()
app.add_handler(CommandHandler("borrar", borrar))
app.add_handler(CallbackQueryHandler(on_borrar, pattern=r"^del:(yes|no):\d+$"))
app.run_polling()

Seguridad: nunca confíes en callback_data

Un cliente modificado puede enviar el callback_data que quiera. Trátalo como entrada del usuario:

  • Comprueba que callback_query.from.id tenga permiso para la acción (por ejemplo, el usuario que pidió borrar, o un administrador).
  • Valida los IDs contra tu base de datos en lugar de confiar en ellos.
  • Para acciones sensibles, guarda en el servidor un token con caducidad y pon solo su clave en el botón.

Patrón de paginación

Para listas largas, envía una página con botones "Anterior" y "Siguiente" cuyo dato sea page:<n>. En cada pulsación, responde la query, carga la página n y edita el mensaje. Mantén las páginas lo bastante cortas para no pasar el límite de 4096 caracteres por mensaje de Telegram; nuestro divisor de mensajes muestra dónde se corta un texto largo.

Nota sobre webhooks

Si tu bot usa webhook, incluye callback_query en allowed_updates o las pulsaciones nunca llegarán. Mira setWebhook de Telegram paso a paso.

Preguntas frecuentes

¿Por qué el botón se queda cargando?

Tu bot no llamó a answerCallbackQuery para esa pulsación, muchas veces porque ningún handler coincidió con los datos. Agrega uno genérico que responda todas las callback queries.

¿Puedo quitar el teclado después de elegir?

Sí. Edita el mensaje con un texto nuevo y sin reply_markup, o llama a editMessageReplyMarkup con un teclado vacío.

¿Los teclados inline funcionan en grupos?

Sí. Cualquiera en el grupo puede pulsar los botones, por eso importa revisar from.id.

Fuentes