Docs para desarrolladores
Español

Desarrollo de bots

Crear un bot

Crea un bot con @BotFather, recibe mensajes y respóndelos, y llega hasta los botones, los comandos, las fotos y los grupos: un tutorial paso a paso de principio a fin.

Al terminar este tutorial tendrás un bot que responde a los mensajes, lleva botones, maneja un menú de comandos y fotos, y también trabaja en grupos. Necesitas la aplicación Pabal, un ordenador donde ejecutar el programa del bot y Python 3.10 o posterior, o bien Node.js 18 o posterior.

Cómo funciona un bot

Un bot de Pabal es una cuenta controlada por un programa. Los mensajes que se envían al bot se acumulan en su buzón de mensajes; el programa del bot le pregunta al servidor «¿ha llegado algo nuevo?» (getUpdates), recoge lo que haya y envía la respuesta (sendMessage). El programa del bot funciona fuera del servidor, en tu ordenador.

Persona (app) Servidor Tu bot ① getUpdates?offset=0&timeout=30 Espera hasta 30 s a que haya novedades ② envía «Hola» ③ [{update_id: 1, message: "Hola"}] ④ sendMessage(chat_id, "¡Hola!") ⑤ La respuesta llega (tiempo real) ⑥ getUpdates?offset=2 — 1 recibida, la siguiente
El orden básico: recibir con getUpdates y responder con sendMessage

Explicación del diagrama

  • Tres columnas: a la izquierda, la persona que usa la aplicación; en el centro, el servidor Pabal; a la derecha, tu programa de bot. Las líneas verticales punteadas indican el paso del tiempo.
  • Las flechas grises son peticiones que hace primero el bot, y las azules, los mensajes que circulan como resultado. El programa del bot solo envía peticiones; el servidor no se pone en contacto con el bot por iniciativa propia (salvo que uses un webhook).
  • La espera de ① (long polling) es la clave: con timeout=30, si no hay novedades el servidor no devuelve enseguida una respuesta vacía, sino que retiene la petición hasta 30 segundos y responde (③) en el momento en que llega un mensaje (②). Así, la reacción es rápida y el número de peticiones, bajo.
  • El offset de ⑥ es la marca de «recibido»: si envías el último update_id procesado más 1, el servidor borra todo lo anterior. Si no subes el offset, seguirás recibiendo la misma actualización.
  • ⑤ sigue el mismo camino que un mensaje enviado por una persona: la respuesta del bot llega como push a todos los dispositivos de la aplicación y también sube en la lista de chats.

1. Crear un bot con @BotFather

Los bots se crean conversando con @BotFather dentro de la aplicación Pabal. BotFather es un bot que viene incluido en el servidor Pabal.

  1. Escribe BotFather en el buscador de la aplicación y abre BotFather. Al pulsar Iniciar te llega la lista de comandos.
  2. Envía /newbot.
  3. Envía el nombre del bot. Es el nombre que se ve en la lista de chats, así que puede llevar tildes, eñes u otros alfabetos.
  4. Envía el nombre de usuario del bot. Tiene entre 5 y 32 caracteres (letras latinas, números y guion bajo), empieza por una letra y tiene que terminar en bot.
  5. Cuando llegue la respuesta con el token, habrás terminado. Copia el token y guárdalo.
/newbot
BotFather새 봇을 만듭니다. 봇의 이름을 알려 주세요. (대화 목록에 보이는 이름이에요)
Hola Bot
BotFather좋아요. 이제 봇의 사용자명을 정해 주세요. …
hello_test_bot
BotFather완료! 새 봇 @hello_test_bot 를 만들었어요. 검색해서 대화를 시작할 수 있어요. 봇 토큰: 100003:AbCdEfGhIjKlMnOpQrStUvWxYz0123456789 토큰은 비밀번호처럼 안전하게 보관하세요. …

Por ahora, BotFather responde en coreano. En la conversación de arriba, primero te pide el nombre del bot (el que se verá en la lista de chats), después te pide que elijas su nombre de usuario y, por último, te confirma que el bot se ha creado, te entrega su token (la línea 100003:…) y te recuerda que lo guardes con el mismo cuidado que una contraseña.

Comando de BotFatherQué hace
/newbotCrear un bot nuevo (nombre → nombre de usuario → token)
/mybotsLista de los bots que has creado
/tokenVolver a ver el token de un bot
/revokeEmitir un token nuevo: el anterior deja de valer al instante y las conexiones abiertas con él también se cortan
/setcommandsConfigurar el menú de comandos (una línea comando - descripción por comando)
/deletebotEliminar un bot: se confirma enviando 네, 삭제합니다 («Sí, elimínalo»). El nombre de usuario queda libre para volver a usarse
/cancelCancelar la operación en curso

Si añades el nombre de usuario, como en /token @hello_test_bot, te saltas el paso de «¿Qué bot?».

2. Manejar el token

El token tiene la forma <ID del bot>:<secreto>. El número de delante es el ID de usuario del bot, y lo que va detrás es el secreto. Con un token se controla el bot por completo, así que trátalo como una contraseña.

  • No lo escribas en el código: guárdalo en una variable de entorno (BOT_TOKEN) o en un gestor de secretos. No lo subas a repositorios públicos.
  • Si se ha filtrado, envía /revoke a BotFather. El token anterior se rechaza al instante (401 Unauthorized) y las sesiones del bot conectadas a MTProto con él también se cortan.
  • El token va dentro de la dirección (URL), así que evita que tu programa de bot registre en los logs las direcciones de las peticiones. El servidor Pabal tampoco registra las direcciones de la Bot API.

3. Primera petición: getMe

La dirección de todas las peticiones es https://pabal.me/bot<token>/<método>. Comprobemos con getMe que el token es correcto.

export BOT_TOKEN='100003:AbCdEf…'
curl -s "https://pabal.me/bot$BOT_TOKEN/getMe"
# pip install requests
import os
import requests

r = requests.get(f"https://pabal.me/bot{os.environ['BOT_TOKEN']}/getMe", timeout=10)
print(r.json())
// Node.js 18 o posterior: fetch viene incluido
const res = await fetch(`https://pabal.me/bot${process.env.BOT_TOKEN}/getMe`);
console.log(await res.json());

Si todo va bien, recibirás esto. Todas las respuestas son JSON con ok y result (en caso de éxito) o error_code y description (en caso de error).

{
  "ok": true,
  "result": {
    "id": 100003,
    "is_bot": true,
    "first_name": "Hola Bot",
    "username": "hello_test_bot",
    "can_join_groups": true,
    "can_read_all_group_messages": true,
    "supports_inline_queries": false,
    "can_connect_to_business": false,
    "has_main_web_app": false
  }
}

Si el token es incorrecto, recibirás un HTTP 401 con {"ok": false, "error_code": 401, "description": "Unauthorized"}.

4. Recibir mensajes: getUpdates

Abre el bot en la aplicación, pulsa Iniciar o envíale cualquier cosa, y después recoge las novedades.

curl -s "https://pabal.me/bot$BOT_TOKEN/getUpdates?timeout=30"
{
  "ok": true,
  "result": [
    {
      "update_id": 1,
      "message": {
        "message_id": 1,
        "from": { "id": 100001, "is_bot": false, "first_name": "Ana" },
        "chat": { "id": 100001, "first_name": "Ana", "type": "private" },
        "date": 1789805661,
        "text": "/start",
        "entities": [ { "type": "bot_command", "offset": 0, "length": 6 } ]
      }
    }
  ]
}
  • update_id: un número que aumenta en 1 con cada actualización. Después de procesarla, si en la siguiente petición pasas offset=update_id+1, todo lo anterior se borra como «recibido».
  • timeout: los segundos que se espera cuando no hay novedades (0–50). Con 0 se devuelve enseguida una lista vacía. Se recomienda entre 25 y 30.
  • chat.id: adónde enviar la respuesta. En un chat uno a uno es el ID de la persona (positivo); en un grupo, un número negativo.
  • Las actualizaciones que puedes recibir son de tres tipos: message (mensaje nuevo), edited_message (mensaje editado) y callback_query (pulsación de un botón).
La cola está en la memoria del servidor

Las actualizaciones que no se recogen se acumulan en la memoria del servidor, hasta las 1.000 más recientes por bot. Si el servidor se reinicia, lo que no se haya recogido se pierde (los mensajes en sí siguen en el chat). No dejes el bot apagado mucho tiempo.

5. Enviar una respuesta: sendMessage

curl -s "https://pabal.me/bot$BOT_TOKEN/sendMessage" \
  -H 'Content-Type: application/json' \
  -d '{"chat_id": 100001, "text": "¡Hola!"}'
requests.post(f"https://pabal.me/bot{os.environ['BOT_TOKEN']}/sendMessage",
              json={"chat_id": 100001, "text": "¡Hola!"}, timeout=10)
await fetch(`https://pabal.me/bot${process.env.BOT_TOKEN}/sendMessage`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ chat_id: 100001, text: '¡Hola!' }),
});

Los parámetros se pueden enviar como prefieras: en un cuerpo JSON, como formulario (application/x-www-form-urlencoded), como multipart/form-data cuando subes archivos o en la cadena de consulta tras la dirección. El resultado es el mensaje enviado (Message).

Un bot no puede iniciar la conversación

Un bot solo puede escribir a una persona después de que esa persona le haya enviado al menos un mensaje. Si no, obtendrás 403 Forbidden: bot can't initiate conversation with a user. Es la misma regla que en Telegram.

6. Un bot que repite lo que le dicen

Repitiendo recibir y enviar ya tienes un bot. Este es el código completo, sin bibliotecas.

# echo.py — pip install requests
# Ejecución: BOT_TOKEN='100003:…' python3 echo.py
import os
import requests

API = f"https://pabal.me/bot{os.environ['BOT_TOKEN']}"


def call(method, **params):
    r = requests.post(f"{API}/{method}", json=params, timeout=60)
    data = r.json()
    if not data["ok"]:
        raise RuntimeError(f"{method}: {data['description']}")
    return data["result"]


offset = 0
print("El bot está en marcha. Pulsa Ctrl+C para detenerlo")
while True:
    for update in call("getUpdates", offset=offset, timeout=30):
        offset = update["update_id"] + 1          # marca de «recibido»
        message = update.get("message")
        if message and "text" in message:
            call("sendMessage", chat_id=message["chat"]["id"], text=message["text"])
// echo.mjs — Node.js 18 o posterior, sin bibliotecas
// Ejecución: BOT_TOKEN='100003:…' node echo.mjs
const API = `https://pabal.me/bot${process.env.BOT_TOKEN}`;

async function call(method, params = {}) {
  const res = await fetch(`${API}/${method}`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(params),
  });
  const data = await res.json();
  if (!data.ok) throw new Error(`${method}: ${data.description}`);
  return data.result;
}

let offset = 0;
console.log('El bot está en marcha. Pulsa Ctrl+C para detenerlo');
for (;;) {
  const updates = await call('getUpdates', { offset, timeout: 30 });
  for (const update of updates) {
    offset = update.update_id + 1;              // marca de «recibido»
    const message = update.message;
    if (message?.text) {
      await call('sendMessage', { chat_id: message.chat.id, text: message.text });
    }
  }
}

7. Crear el bot con una biblioteca

Las bibliotecas de bots para Telegram tienen una opción para cambiar la dirección del servidor. Con esa sola opción funcionan tal cual en Pabal. Para trasladar un bot hecho para Telegram, cambia solo eso y pide un token nuevo al BotFather de Pabal.

BibliotecaQué cambiarVersión comprobada
python-telegram-bot.base_url("https://pabal.me/bot"), .base_file_url("https://pabal.me/file/bot")22.8
aiogramAiohttpSession(api=TelegramAPIServer.from_base("https://pabal.me"))3.31
Sin biblioteca (HTTP)El principio de la dirección: https://api.telegram.orghttps://pabal.me
# hello_bot.py — pip install python-telegram-bot
# Ejecución: BOT_TOKEN='100003:…' python3 hello_bot.py
import os

from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update
from telegram.ext import (Application, CallbackQueryHandler, CommandHandler, ContextTypes,
                          MessageHandler, filters)

SERVER = "https://pabal.me"


def buttons():
    return InlineKeyboardMarkup([[InlineKeyboardButton("👍 Me gusta", callback_data="like"),
                                  InlineKeyboardButton("🔢 Sumar uno", callback_data="count")]])


async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
    await update.message.reply_text("¡Hola! Prueba a pulsar los botones.", reply_markup=buttons())


async def button(update: Update, context: ContextTypes.DEFAULT_TYPE):
    query = update.callback_query
    if query.data == "like":
        await query.answer("¡Gracias!")                  # texto que aparece un momento en la pantalla de quien pulsó
    else:
        n = context.chat_data.get("n", 0) + 1
        context.chat_data["n"] = n
        await query.answer()                             # primero respondes
        await query.edit_message_text(f"Número: {n}", reply_markup=buttons())   # y luego editas el mensaje


async def echo(update: Update, context: ContextTypes.DEFAULT_TYPE):
    await update.message.reply_text(f"Has dicho «{update.message.text}».")


def main():
    app = (Application.builder().token(os.environ["BOT_TOKEN"])
           .base_url(f"{SERVER}/bot")                   # Pabal en lugar de api.telegram.org
           .base_file_url(f"{SERVER}/file/bot")
           .build())
    app.add_handler(CommandHandler("start", start))
    app.add_handler(CallbackQueryHandler(button))
    app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, echo))
    print("El bot está en marcha. Pulsa Ctrl+C para detenerlo")
    app.run_polling()


if __name__ == "__main__":
    main()
# echo_aiogram.py — pip install aiogram
# Ejecución: BOT_TOKEN='100003:…' python3 echo_aiogram.py
import asyncio
import os

from aiogram import Bot, Dispatcher
from aiogram.client.session.aiohttp import AiohttpSession
from aiogram.client.telegram import TelegramAPIServer
from aiogram.filters import CommandStart

dp = Dispatcher()


@dp.message(CommandStart())
async def start(message):
    await message.answer("¡Hola! Envíame lo que quieras.")


@dp.message()
async def echo(message):
    if message.text:
        await message.answer(message.text)


async def main():
    session = AiohttpSession(api=TelegramAPIServer.from_base("https://pabal.me"))   # Pabal
    bot = Bot(os.environ["BOT_TOKEN"], session=session)
    print("El bot está en marcha. Pulsa Ctrl+C para detenerlo")
    await dp.start_polling(bot)


asyncio.run(main())

8. Botones y callbacks

Para añadir botones inline a un mensaje, pasa en reply_markup un inline_keyboard (un array de filas; cada fila es un array de botones). Hay dos tipos de botón: los de callback_data, que avisan al bot cuando se pulsan, y los de url, que abren un enlace.

curl -s "https://pabal.me/bot$BOT_TOKEN/sendMessage" -H 'Content-Type: application/json' -d '{
  "chat_id": 100001,
  "text": "Elige una opción",
  "reply_markup": {
    "inline_keyboard": [
      [ {"text": "👍 Me gusta", "callback_data": "like"}, {"text": "👎 No mucho", "callback_data": "dislike"} ],
      [ {"text": "Abrir la documentación de Pabal", "url": "https://pabal.me/docs/"} ]
    ]
  }
}'

Cuando alguien pulsa un botón de callback_data, el bot recibe una actualización callback_query.

{
  "update_id": 7,
  "callback_query": {
    "id": "5812039457730125441",
    "from": { "id": 100001, "is_bot": false, "first_name": "Ana" },
    "message": { "message_id": 4, "chat": { "id": 100001, "type": "private", "first_name": "Ana" }, "text": "Elige una opción", … },
    "chat_instance": "8413962145072395171",
    "data": "like"
  }
}

El bot debe responder con answerCallbackQuery en menos de 10 segundos. Mientras tanto, la aplicación muestra un reloj en el botón y espera.

# Texto que aparece un momento sobre la pantalla (con show_alert: true, un cuadro de confirmación)
curl -s "https://pabal.me/bot$BOT_TOKEN/answerCallbackQuery" -H 'Content-Type: application/json' \
  -d '{"callback_query_id": "5812039457730125441", "text": "¡Gracias!"}'

# Cambiar el texto y los botones del mensaje pulsado
curl -s "https://pabal.me/bot$BOT_TOKEN/editMessageText" -H 'Content-Type: application/json' \
  -d '{"chat_id": 100001, "message_id": 4, "text": "Has pulsado Me gusta 👍"}'
  • callback_data ocupa entre 1 y 64 bytes. Un mensaje puede llevar como máximo 100 botones.
  • Si no respondes al callback, la aplicación deja de esperar a los 10 segundos. Si el bot está apagado, el servidor termina la espera enseguida.
  • Solo vale la primera respuesta. Algunas bibliotecas envían primero una respuesta vacía al editar el mensaje, así que envía primero la respuesta con texto.
  • Si editas con el mismo texto y los mismos botones, obtendrás 400 Bad Request: message is not modified (igual que en Telegram).

Teclado bajo el campo de texto

Si pasas keyboard, en lugar del campo de texto aparece un panel de botones, y al pulsar uno su texto se envía como mensaje. Con remove_keyboard se oculta y con force_reply se activa el modo de respuesta.

{
  "chat_id": 100001,
  "text": "¿Cuál prefieres?",
  "reply_markup": {
    "keyboard": [ [ {"text": "Sí"}, {"text": "No"} ], [ {"text": "Enviar mi ubicación", "request_location": true} ] ],
    "resize_keyboard": true,
    "one_time_keyboard": true
  }
}

9. Menú de comandos

Es la lista que aparece al pulsar / en el chat o el botón Menú. Se define por código o con /setcommands de BotFather.

curl -s "https://pabal.me/bot$BOT_TOKEN/setMyCommands" -H 'Content-Type: application/json' -d '{
  "commands": [
    {"command": "start", "description": "Empezar"},
    {"command": "help",  "description": "Ayuda"}
  ]
}'

Un comando tiene entre 1 y 32 caracteres (letras latinas minúsculas, números y guion bajo), la descripción entre 1 y 256 caracteres, y puede haber como máximo 100 comandos. Si pasas language_code, se guarda una lista aparte para cada idioma, pero por ahora la aplicación solo muestra la lista predeterminada, la definida sin código de idioma. Los comandos que envía una persona, como /start, llegan marcados como bot_command en las entities del mensaje.

10. Enviar y recibir fotos

Enviar

Sube el archivo como multipart/form-data o reutiliza el file_id de una foto recibida antes. Las fotos pueden ocupar hasta 10 MB y el pie de foto (caption) hasta 1.024 caracteres. El envío mediante URL todavía no está disponible.

curl -s "https://pabal.me/bot$BOT_TOKEN/sendPhoto" \
  -F chat_id=100001 -F caption='La foto de hoy' -F photo=@sunset.jpg
with open("sunset.jpg", "rb") as f:
    requests.post(f"{API}/sendPhoto", data={"chat_id": 100001, "caption": "La foto de hoy"},
                  files={"photo": f}, timeout=60)

Recibir

Las fotos que envía una persona llegan en el campo photo del mensaje (una lista por tamaños; en Pabal, un único elemento con el original). Obtén la ruta con getFile y descárgala.

# 1) file_id → file_path
curl -s "https://pabal.me/bot$BOT_TOKEN/getFile?file_id=AQAAAAAAAAB7…"
# {"ok":true,"result":{"file_id":"AQAA…","file_unique_id":"AQAA…","file_size":48213,"file_path":"photos/AQAA….jpg"}}

# 2) Descargar: la dirección lleva /file/
curl -s -o photo.jpg "https://pabal.me/file/bot$BOT_TOKEN/photos/AQAA….jpg"

11. En grupos

  • Al crear un grupo en la aplicación, o desde la información del grupo → Añadir miembros, busca el nombre de usuario del bot y añádelo.
  • Un bot que está en un grupo recibe todos los mensajes del grupo (como con el «modo de privacidad desactivado» de Telegram). chat.type es "group" y chat.id es negativo.
  • Si haces sendMessage con ese chat.id, el mensaje se envía al grupo. Los botones, las fotos y las ediciones funcionan igual que en un chat uno a uno.
  • Si el bot sale del grupo, los envíos a ese grupo devuelven 403 Forbidden: bot is not a member of the group chat.

12. Cambiar a webhook

Si tu bot funciona en un servidor con una dirección HTTPS pública, en lugar de preguntar con getUpdates puedes hacer que el servidor Pabal envíe las novedades a esa dirección.

curl -s "https://pabal.me/bot$BOT_TOKEN/setWebhook" -H 'Content-Type: application/json' \
  -d '{"url": "https://bot.example.com/pabal-webhook", "secret_token": "cadena-larga-aleatoria"}'

La configuración, la verificación, los reintentos y cómo responder dentro de la propia respuesta se explican en el documento Webhooks.

Bots que se conectan por MTProto (Telethon)

Un bot también puede conectarse por MTProto, como la aplicación. Es cómodo si ya usas herramientas para cuentas de persona (Telethon, por ejemplo). Como es la misma cuenta de bot, puedes combinarlo con HTTP.

# pip install telethon==1.42.0   ← usa la 1.42 (explicación más abajo)
# Clave pública del servidor: descarga https://pabal.me/docs/server-key.pem en la misma carpeta
import asyncio
import os

from telethon import TelegramClient, events
from telethon.crypto import rsa
from telethon.sessions import StringSession

rsa.add_key(open("server-key.pem").read(), old=False)        # clave pública del servidor Pabal
client = TelegramClient(StringSession(), api_id=1, api_hash="0" * 32)
client.session.set_dc(2, "122.34.175.215", 8443)


@client.on(events.NewMessage(incoming=True))
async def echo(event):
    await event.reply(event.raw_text)


async def main():
    await client.start(bot_token=os.environ["BOT_TOKEN"])      # auth.importBotAuthorization
    print("El bot está en marcha. Pulsa Ctrl+C para detenerlo")
    await client.run_until_disconnected()


asyncio.run(main())
  • Usa Telethon 1.42. Pabal habla la capa 216, y las versiones más recientes de Telethon intentan leer las respuestas con una capa superior, así que fallan ya en el inicio de sesión (TypeNotFoundError).
  • Pabal no comprueba api_id ni api_hash, así que sirve cualquier valor.
  • A los bots MTProto el servidor les envía los mensajes nuevos en tiempo real (no hacen falta getUpdates ni webhooks). Para responder a un callback, llama a event.answer("…") antes que a event.edit(…).

Reglas y límites

ConceptoValor
Caracteres por mensaje / pie de foto4.096 / 1.024 caracteres
Tamaño de foto (subida con sendPhoto)10 MB
Tamaño del cuerpo de la petición12 MB
timeout · limit de getUpdates0–50 segundos · 1–100 elementos
Conservación de actualizaciones no recogidasLas 1.000 más recientes por bot, en la memoria del servidor (se pierden al reiniciar)
Espera de la respuesta a un callback10 segundos
callback_data · número de botones1–64 bytes · 100 por mensaje
Comandos1–32 caracteres (letras latinas minúsculas, números y guion bajo), descripción de 1–256 caracteres, 100 como máximo
Iniciar la conversaciónNo se puede: la persona tiene que escribir antes al bot

Solución de problemas

SíntomaCausa y solución
401 UnauthorizedEl token es incorrecto o se cambió con /revoke. Envía /token a BotFather para comprobarlo.
404 Not Found: method not foundEs un método que Pabal todavía no admite. Consulta la lista de métodos.
403 Forbidden: bot can't initiate conversation with a userEsa persona todavía no ha escrito nunca al bot. Pídele que abra el bot en la aplicación y pulse Iniciar.
409 Conflict: can't use getUpdates method while webhook is activeHay un webhook configurado. Llama a deleteWebhook o recibe por webhook.
No pasa nada al pulsar un botónEl bot está apagado o no ha llamado a answerCallbackQuery.
Recibo el mismo mensaje una y otra vezNo has subido el offset. Pasa en la siguiente petición el update_id + 1 que procesaste.
La negrita o los enlaces con formato no funcionanparse_mode todavía no está disponible, así que el texto se envía tal cual. Los comandos, las @menciones, las URL y las #etiquetas se marcan automáticamente como pulsables.
El bot no dice nada en el grupoEl bot no es miembro del grupo. Añádelo desde la información del grupo → Añadir miembros.