Docs para desarrolladores
Español

Desarrollo de bots

Webhooks

Cómo hacer que, en lugar de usar getUpdates, el servidor envíe las novedades directamente a la dirección HTTPS de tu bot: configuración, verificación, reintentos y respuesta dentro de la propia respuesta.

getUpdates es un modo en el que el bot pregunta al servidor una y otra vez. Un webhook funciona al revés: cuando hay novedades, el servidor Pabal las envía directamente a la dirección HTTPS del bot. Si tu bot funciona en un servidor que está siempre encendido (en la nube o en la propia empresa), el webhook es más adecuado.

getUpdates (long polling)Webhook
Quién da el primer pasoEl bot pregunta al servidorEl servidor envía al bot
Qué necesita el botSolo acceso saliente a internetUna dirección HTTPS pública
Adecuado paraTu ordenador, desarrollo, detrás de un cortafuegosServidores siempre encendidos, serverless, muchos bots
Usar ambos a la vezNo se puede: si hay un webhook configurado, getUpdates devuelve 409 Conflict

Así funciona un webhook

Servidor Pabal Tu webhook (HTTPS) ① POST actualización 5 · X-Telegram-Bot-Api-Secret-Token ② 200 OK → 5 confirmada, sigue ③ POST actualización 6 ④ 500 · sin conexión · >30 s → anota last_error Cada 1 s → 2 s → 4 s … hasta 60 s reenvía la 6 (la 7 espera tras la 6) ⑤ POST actualización 6 (otra vez) ⑥ 200 + {"method": "sendMessage", "chat_id": …, "text": …} Ejecuta el método de la respuesta por el bot → la respuesta va a la app ⑦ POST actualización 7 …
Las actualizaciones de un bot se envían de una en una y en orden

Explicación del diagrama

  • Dos columnas: a la izquierda, el servidor Pabal; a la derecha, la dirección de webhook que operas tú. La línea continua azul son las peticiones que envía el servidor; la gris, las respuestas normales del webhook; la discontinua roja, los fallos.
  • Una respuesta 2xx significa «recibido» (②). Hasta entonces, la actualización se queda en el servidor. El cuerpo puede ir vacío.
  • Si falla, se reenvía la misma actualización (④ → ⑤): una respuesta que no sea 2xx, un fallo de conexión o la falta de respuesta en 30 segundos cuentan como fallo, y el intervalo empieza en 1 segundo y se duplica hasta un máximo de 60 segundos. El motivo y la hora del fallo quedan en getWebhookInfo.
  • Se respeta el orden: las actualizaciones de un bot se envían de una en una, así que la 7 espera hasta que la 6 tiene éxito. Por eso, si el webhook falla durante mucho tiempo, las actualizaciones siguientes se acumulan (pending_update_count).
  • Puedes responder dentro de la respuesta (⑥): si en el cuerpo de la respuesta 200 incluyes method y sus parámetros, el servidor ejecuta ese método en nombre del bot. Es más rápido, porque no hace falta enviar otra petición.

Requisitos de la dirección del webhook

  • Tiene que ser una dirección https://. El certificado debe proceder de una autoridad de certificación pública (Let's Encrypt, por ejemplo); no se admite subir un certificado autofirmado (certificate).
  • Tiene que ser una dirección pública accesible desde internet. El servidor no envía a direcciones internas como loopback (127.0.0.1, ::1), redes privadas (10., 172.16–31., 192.168.), link-local (169.254., metadatos de la nube) o CGNAT (100.64/10). También rechaza los dominios que apuntan a esas direcciones, y lo vuelve a comprobar en cada envío.
  • No hay restricción de puerto (no tiene por qué ser el 443). La dirección no puede incluir usuario ni contraseña (https://user:pw@…).
  • Las redirecciones (3xx) no se siguen y cuentan como fallo. Indica la dirección final.
  • Hay que responder en menos de 30 segundos. Si una tarea tarda, responde primero con un 200 y procésala después en segundo plano.

Configuración

  1. Pon en marcha el programa que recibirá el webhook. Supongamos que lanzas uno de los ejemplos de más abajo en el servidor del bot, en 127.0.0.1:8081.
  2. Coloca HTTPS delante. Por ejemplo, con Caddy bastan estas dos líneas, y el certificado se gestiona automáticamente.
    bot.example.com {
        reverse_proxy 127.0.0.1:8081
    }
  3. Crea un token secreto. De 1 a 256 caracteres entre letras latinas, números, _ y -. El servidor envía este valor en una cabecera con cada petición, así que puedes comprobar que la petición viene de verdad del servidor Pabal.
    export WEBHOOK_SECRET=$(openssl rand -hex 32)
  4. Llama a setWebhook.
    curl -s "https://pabal.me/bot$BOT_TOKEN/setWebhook" -H 'Content-Type: application/json' -d "{
      \"url\": \"https://bot.example.com/pabal-webhook\",
      \"secret_token\": \"$WEBHOOK_SECRET\",
      \"allowed_updates\": [\"message\", \"callback_query\"],
      \"drop_pending_updates\": true
    }"
    # {"ok":true,"result":true}
  5. Comprueba el estado. Envía un mensaje al bot desde la aplicación y consulta getWebhookInfo.
    curl -s "https://pabal.me/bot$BOT_TOKEN/getWebhookInfo"
    {
      "ok": true,
      "result": {
        "url": "https://bot.example.com/pabal-webhook",
        "has_custom_certificate": false,
        "pending_update_count": 0,
        "ip_address": "198.51.100.7",
        "max_connections": 40,
        "allowed_updates": ["message", "callback_query"]
      }
    }

    Si pending_update_count es 0, está recibiendo bien. last_error_date y last_error_message son el registro del último fallo y se mantienen incluso después de recuperarse: fíjate en la hora para valorarlos.

Parámetros de setWebhook

ParámetroTipoObligatorioDescripción
urlStringDirección del webhook. Una cadena vacía elimina el webhook (igual que deleteWebhook).
secret_tokenStringOpcionalDe 1 a 256 caracteres, A-Z a-z 0-9 _ -. Se envía con cada petición en la cabecera X-Telegram-Bot-Api-Secret-Token.
allowed_updatesArray of StringOpcionalTipos que quieres recibir: message, edited_message, callback_query. Si lo dejas vacío, todos. Los tipos que no estén incluidos no se envían y se descartan.
drop_pending_updatesBooleanOpcionalCon true, se descartan todas las actualizaciones aún no entregadas antes de empezar.
max_connectionsIntegerOpcionalDe 1 a 100, 40 por defecto. Se acepta y se guarda, pero Pabal envía de una en una por bot para respetar el orden.
certificateInputFileNo admitidoNo se aceptan certificados autofirmados (400). Usa un certificado público.
ip_addressStringSe ignoraSe acepta pero no se usa. El servidor resuelve el dominio en cada momento.

La configuración del webhook se guarda en el servidor, así que se mantiene aunque el servidor se reinicie, y el envío se reanuda en cuanto arranca. Eso sí, las actualizaciones aún no entregadas están en memoria y se pierden al reiniciar.

La petición que envía el servidor

POST /pabal-webhook HTTP/1.1
Host: bot.example.com
Content-Type: application/json
X-Telegram-Bot-Api-Secret-Token: 3f1c…(el valor que pasaste a setWebhook)

{"update_id":12,"message":{"message_id":3,"from":{"id":100001,"is_bot":false,"first_name":"Ana"},"chat":{"id":100001,"first_name":"Ana","type":"private"},"date":1789805661,"text":"Hola"}}
  • El cuerpo es un objeto Update, idéntico a un elemento del resultado de getUpdates.
  • Comprueba siempre el token secreto. Si falta o no coincide, rechaza la petición con un 401. Compara con una función de tiempo constante (hmac.compare_digest, crypto.timingSafeEqual).
  • La misma actualización puede llegar dos veces (si la conexión se corta antes de que tu respuesta llegue al servidor). Es seguro saltarse, por su update_id, las que ya hayas procesado.

Responder dentro de la respuesta

Si incluyes una llamada a la Bot API en el cuerpo de la respuesta 200, el servidor la ejecuta en nombre del bot. Pon el nombre del método en method y añade el resto de los parámetros.

{"method": "sendMessage", "chat_id": 100001, "text": "¡Hola!"}
  • El cuerpo puede ir en JSON, como formulario o como multipart/form-data (aiogram lo envía como multipart). El tamaño máximo es de 1 MB.
  • El resultado o el error de esta llamada no se devuelven al bot (solo quedan en el log del servidor). Si necesitas el resultado, haz la petición aparte como de costumbre.
  • Si no tienes nada que responder, basta con un 200 sin cuerpo.

Ejemplos

Los cuatro ejemplos comprueban el token secreto y, al recibir un mensaje de texto, lo repiten incluyendo un sendMessage en la respuesta. Se supone que hay HTTPS delante (Caddy, por ejemplo) y que reciben en 127.0.0.1:8081.

# webhook.py — solo con la biblioteca estándar
# Ejecución: WEBHOOK_SECRET='…' python3 webhook.py
import hmac
import json
import os
from http.server import BaseHTTPRequestHandler, HTTPServer

SECRET = os.environ["WEBHOOK_SECRET"]


class Webhook(BaseHTTPRequestHandler):
    def do_POST(self):
        got = self.headers.get("X-Telegram-Bot-Api-Secret-Token", "")
        if not hmac.compare_digest(got, SECRET):
            self.send_response(401)
            self.end_headers()
            return
        update = json.loads(self.rfile.read(int(self.headers["Content-Length"])))
        message = update.get("message")
        answer = b""
        if message and "text" in message:
            answer = json.dumps({"method": "sendMessage",
                                 "chat_id": message["chat"]["id"],
                                 "text": message["text"]}).encode()
        self.send_response(200)
        self.send_header("Content-Type", "application/json")
        self.send_header("Content-Length", str(len(answer)))
        self.end_headers()
        self.wfile.write(answer)


HTTPServer(("127.0.0.1", 8081), Webhook).serve_forever()
// webhook.mjs — Node.js 18 o posterior, sin bibliotecas
// Ejecución: WEBHOOK_SECRET='…' node webhook.mjs
import http from 'node:http';
import { timingSafeEqual } from 'node:crypto';

const SECRET = Buffer.from(process.env.WEBHOOK_SECRET);

http.createServer(async (req, res) => {
  const got = Buffer.from(req.headers['x-telegram-bot-api-secret-token'] ?? '');
  if (req.method !== 'POST' || got.length !== SECRET.length || !timingSafeEqual(got, SECRET)) {
    res.writeHead(401).end();
    return;
  }
  let body = '';
  for await (const chunk of req) body += chunk;
  const message = JSON.parse(body).message;
  if (message?.text) {
    res.writeHead(200, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify({ method: 'sendMessage', chat_id: message.chat.id, text: message.text }));
  } else {
    res.writeHead(200).end();
  }
}).listen(8081, '127.0.0.1');
# webhook_aiogram.py — pip install aiogram
# Este programa también configura el webhook (setWebhook)
import os

from aiogram import Bot, Dispatcher
from aiogram.client.session.aiohttp import AiohttpSession
from aiogram.client.telegram import TelegramAPIServer
from aiogram.webhook.aiohttp_server import SimpleRequestHandler, setup_application
from aiohttp import web

URL = "https://bot.example.com/pabal-webhook"
SECRET = os.environ["WEBHOOK_SECRET"]
dp = Dispatcher()


@dp.message()
async def echo(message):
    if message.text:
        return message.answer(message.text)       # return: se envía dentro de la respuesta del webhook


async def on_startup(bot: Bot):
    await bot.set_webhook(URL, secret_token=SECRET)


def main():
    session = AiohttpSession(api=TelegramAPIServer.from_base("https://pabal.me"))
    bot = Bot(os.environ["BOT_TOKEN"], session=session)
    dp.startup.register(on_startup)
    app = web.Application()
    SimpleRequestHandler(dispatcher=dp, bot=bot, secret_token=SECRET).register(app, path="/pabal-webhook")
    setup_application(app, dp, bot=bot)
    web.run_app(app, host="127.0.0.1", port=8081)


main()
# webhook_ptb.py — pip install "python-telegram-bot[webhooks]"
# Este programa también configura el webhook (setWebhook)
import os

from telegram import Update
from telegram.ext import Application, ContextTypes, MessageHandler, filters

SERVER = "https://pabal.me"


async def echo(update: Update, context: ContextTypes.DEFAULT_TYPE):
    await update.message.reply_text(update.message.text)


app = (Application.builder().token(os.environ["BOT_TOKEN"])
       .base_url(f"{SERVER}/bot").base_file_url(f"{SERVER}/file/bot").build())
app.add_handler(MessageHandler(filters.TEXT, echo))
app.run_webhook(listen="127.0.0.1", port=8081, url_path="pabal-webhook",
                webhook_url="https://bot.example.com/pabal-webhook",
                secret_token=os.environ["WEBHOOK_SECRET"])

Ver el estado: getWebhookInfo

CampoSignificado
urlLa dirección configurada. Si no hay webhook, una cadena vacía
pending_update_countNúmero de actualizaciones que esperan a ser entregadas
ip_addressLa IP de la última dirección a la que se envió
last_error_date, last_error_messageLa hora (segundos Unix) y el motivo del último fallo. No aparecen si nunca ha fallado. Se mantienen después de recuperarse
max_connections, allowed_updatesLos valores que pasaste a setWebhook
has_custom_certificateSiempre false

Mensajes que aparecen en last_error_message

MensajeCausa
Connection refusedNadie escucha en esa dirección y puerto. El programa del webhook o el proxy están apagados
Connection timed outUn cortafuegos lo bloquea o la dirección no es alcanzable
Read timeout expiredNo respondió en 30 segundos
Failed to resolve host: Name or service not knownNo se encuentra el nombre de dominio (DNS)
SSL error {…}Problema con el certificado: caducado, autofirmado o con un nombre que no coincide
Wrong response from the webhook: 502 Bad GatewayUna respuesta que no es 2xx. El número es el código de estado que devolvió el webhook
IP address 10.0.0.5 is reservedEl dominio apunta a una dirección interna (no se envía)

Volver a getUpdates

curl -s "https://pabal.me/bot$BOT_TOKEN/deleteWebhook"
# Para descartar también las actualizaciones pendientes: deleteWebhook?drop_pending_updates=true

Las actualizaciones que el webhook ya recibió (respondiendo con 2xx) no vuelven a llegar por getUpdates. Por getUpdates solo sigues recibiendo las que aún no se habían entregado.

Probar durante el desarrollo

  • Túnel: con una herramienta que da una dirección HTTPS pública a 127.0.0.1:8081 de tu ordenador (cloudflared, ngrok, etc.) también puedes probar contra el servidor Pabal de producción.
  • Tu propio servidor Pabal: si has levantado tu propio servidor Pabal de desarrollo, actívalo con TELEGRAM_WEBHOOK_ALLOW_LOCAL=true. Ese servidor también enviará a direcciones locales como http://127.0.0.1:8081/…. No lo actives nunca en un servidor de producción: cualquier bot podría enviar peticiones a la red interna del servidor (base de datos, metadatos de la nube).

Lista de comprobación para producción

  • Has definido un token secreto y lo compruebas en todas las peticiones.
  • Respondes en menos de 30 segundos: las tareas largas pasan a una cola y respondes enseguida con un 200.
  • Filtras los duplicados por update_id.
  • Si el webhook se detiene mucho tiempo, las actualizaciones siguientes se acumulan y se pierden al reiniciar el servidor: vigila el programa del webhook (pending_update_count y last_error_date de getWebhookInfo).
  • Si el token se filtra, usa /revoke y vuelve a llamar a setWebhook con el token nuevo. Cambia también el token secreto.