Docs développeurs
Français

Développement de bots

Webhooks

Au lieu de getUpdates, laissez le serveur envoyer directement les nouveautés à l'adresse HTTPS de votre bot : configuration, vérification, nouvelles tentatives, réponse dans la réponse HTTP.

Avec getUpdates, c'est le bot qui interroge sans cesse le serveur. Le webhook fonctionne à l'inverse : dès qu'il y a du nouveau, le serveur Pabal l'envoie directement à l'adresse HTTPS du bot. Si votre bot tourne sur un serveur toujours allumé (cloud, serveur d'entreprise), le webhook est plus adapté.

getUpdates (long polling)Webhook
Qui prend l'initiativeLe bot interroge le serveurLe serveur envoie au bot
Ce dont le bot a besoinUn simple accès Internet sortantUne adresse HTTPS publique
Idéal pourVotre ordinateur, le développement, derrière un pare-feuUn serveur toujours allumé, du serverless, plusieurs bots
Les deux à la foisImpossible — quand un webhook est configuré, getUpdates renvoie 409 Conflict

Comment circule un webhook

Serveur Pabal Webhook (HTTPS) ① POST mise à jour 5 · X-Telegram-Bot-Api-Secret-Token ② 200 OK → 5 confirmée, suivante ③ POST mise à jour 6 ④ 500 · échec connexion · > 30 s → last_error Intervalle 1 s → 2 s → 4 s … 60 s max On renvoie la 6 (la 7 attend son tour) ⑤ POST mise à jour 6 (à nouveau) ⑥ 200 + {"method": "sendMessage", "chat_id": …, "text": …} Exécute la méthode reçue pour le bot → la réponse arrive dans l'app ⑦ POST mise à jour 7 …
Les mises à jour d'un bot partent une par une, dans l'ordre

Explication du diagramme

  • Deux colonnes : à gauche le serveur Pabal, à droite l'adresse de webhook que vous exploitez. Les traits pleins bleus sont les requêtes envoyées par le serveur, les flèches grises les réponses normales du webhook, les pointillés rouges les échecs.
  • Une réponse 2xx signifie « reçu » (②). Jusque-là, la mise à jour reste sur le serveur. Le corps de la réponse peut être vide.
  • En cas d'échec, la même mise à jour est renvoyée (④ → ⑤) : une réponse autre que 2xx, un échec de connexion ou l'absence de réponse dans les 30 secondes comptent tous comme un échec, et l'intervalle double à partir de 1 seconde, jusqu'à 60 secondes au maximum. La raison et l'heure de l'échec sont consignées dans getWebhookInfo.
  • L'ordre est respecté : les mises à jour d'un bot partent une par une, donc la 7 attend que la 6 ait réussi. Si le webhook échoue longtemps, les mises à jour suivantes s'accumulent (pending_update_count).
  • Vous pouvez répondre dans la réponse (⑥) : si le corps de la réponse 200 contient method et ses paramètres, le serveur exécute cette méthode à la place du bot. Pas besoin d'envoyer une requête supplémentaire : c'est plus rapide.

Conditions pour l'adresse du webhook

  • Ce doit être une adresse https://. Le certificat doit provenir d'une autorité de certification publique (Let's Encrypt, etc.) ; l'envoi d'un certificat auto-signé (certificate) n'est pas pris en charge.
  • Ce doit être une adresse publique joignable depuis Internet. Le serveur n'envoie rien vers des adresses internes comme la boucle locale (127.0.0.1, ::1), les réseaux privés (10., 172.16–31., 192.168.), le lien local (169.254., métadonnées cloud) ou le CGNAT (100.64/10). Il refuse aussi un domaine qui pointe vers une telle adresse, et vérifie de nouveau à chaque envoi.
  • Le port est libre (pas forcément 443). L'adresse ne peut pas contenir de nom d'utilisateur ni de mot de passe (https://user:pw@…).
  • Les redirections (3xx) ne sont pas suivies et comptent comme un échec. Indiquez l'adresse finale.
  • Vous devez répondre dans les 30 secondes. Pour un traitement long, répondez d'abord 200, puis traitez en arrière-plan.

Configuration

  1. Lancez le programme qui recevra le webhook. Supposons que vous lanciez l'un des exemples ci-dessous sur le serveur du bot, sur 127.0.0.1:8081.
  2. Placez HTTPS devant. Avec Caddy, par exemple, ces deux lignes suffisent, certificat compris.
    bot.example.com {
        reverse_proxy 127.0.0.1:8081
    }
  3. Créez un jeton secret. De 1 à 256 caractères parmi les lettres latines, les chiffres, _ et -. Le serveur place cette valeur dans un en-tête de chaque requête : vous pouvez ainsi vérifier que la requête vient bien du vrai serveur Pabal.
    export WEBHOOK_SECRET=$(openssl rand -hex 32)
  4. Appelez 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. Vérifiez l'état. Envoyez un message au bot depuis l'application, puis consultez 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 vaut 0, tout est bien reçu. last_error_date et last_error_message sont la trace du dernier échec et restent présents même une fois le problème résolu : jugez d'après l'heure.

Paramètres de setWebhook

ParamètreTypeObligatoireDescription
urlStringOuiAdresse du webhook. Une chaîne vide supprime le webhook (comme deleteWebhook).
secret_tokenStringFacultatif1 à 256 caractères, A-Z a-z 0-9 _ -. Envoyé dans l'en-tête X-Telegram-Bot-Api-Secret-Token de chaque requête.
allowed_updatesArray of StringFacultatifTypes à recevoir : message, edited_message, callback_query. Vide = tous. Les types absents ne sont pas envoyés mais jetés.
drop_pending_updatesBooleanFacultatifSi true, toutes les mises à jour pas encore livrées sont jetées avant de commencer.
max_connectionsIntegerFacultatif1 à 100, 40 par défaut. Accepté et conservé, mais pour respecter l'ordre, Pabal n'envoie qu'une requête à la fois par bot.
certificateInputFileNon pris en chargeLes certificats auto-signés ne sont pas acceptés (400). Utilisez un certificat public.
ip_addressStringIgnoréAccepté mais non utilisé. Le serveur résout le domaine à chaque envoi.

La configuration du webhook est enregistrée sur le serveur et survit à un redémarrage : les envois reprennent dès le démarrage. En revanche, les mises à jour pas encore livrées sont en mémoire et sont perdues au redémarrage.

La requête envoyée par le serveur

POST /pabal-webhook HTTP/1.1
Host: bot.example.com
Content-Type: application/json
X-Telegram-Bot-Api-Secret-Token: 3f1c…(valeur passée à setWebhook)

{"update_id":12,"message":{"message_id":3,"from":{"id":100001,"is_bot":false,"first_name":"Léa"},"chat":{"id":100001,"first_name":"Léa","type":"private"},"date":1789805661,"text":"Salut"}}
  • Le corps est un objet Update, identique à un élément du résultat de getUpdates.
  • Vérifiez toujours le jeton secret. Si l'en-tête est absent ou différent, refusez avec 401. Comparez avec une fonction à temps constant (hmac.compare_digest, crypto.timingSafeEqual).
  • La même mise à jour peut arriver deux fois (si la connexion a été coupée avant que votre réponse n'atteigne le serveur). Pour être tranquille, ignorez grâce à update_id ce que vous avez déjà traité.

Répondre dans la réponse

Si le corps de la réponse 200 contient un appel à la Bot API, le serveur exécute cet appel à la place du bot. Mettez le nom de la méthode dans method, avec les autres paramètres.

{"method": "sendMessage", "chat_id": 100001, "text": "Bonjour !"}
  • Le corps peut être en JSON, en formulaire ou en multipart/form-data (aiogram envoie du multipart). Taille maximale : 1 Mo.
  • Le résultat ou l'erreur de cet appel ne revient pas au bot (il apparaît seulement dans les journaux du serveur). Si vous avez besoin du résultat, faites une requête séparée comme d'habitude.
  • Si vous n'avez rien à répondre, renvoyez simplement 200 sans corps.

Exemples

Les quatre exemples vérifient le jeton secret et, lorsqu'ils reçoivent un message texte, le répètent en plaçant sendMessage dans la réponse. On suppose qu'un frontal HTTPS (Caddy, par exemple) est placé devant eux et qu'ils écoutent sur 127.0.0.1:8081.

# webhook.py — bibliothèque standard uniquement
# Lancement : 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 ou plus récent, sans bibliothèque
// Lancement : 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
# Ce programme configure aussi le 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 : envoyé dans la réponse du 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]"
# Ce programme configure aussi le 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"])

Consulter l'état — getWebhookInfo

ChampSignification
urlAdresse configurée. Chaîne vide s'il n'y a pas de webhook
pending_update_countNombre de mises à jour en attente, pas encore livrées
ip_addressIP de l'adresse vers laquelle le dernier envoi a eu lieu
last_error_date, last_error_messageHeure (secondes Unix) et raison du dernier échec. Absents s'il n'y a jamais eu d'échec. Restent présents une fois le problème résolu
max_connections, allowed_updatesValeurs passées à setWebhook
has_custom_certificateToujours false

Messages possibles dans last_error_message

MessageCause
Connection refusedRien n'écoute sur cette adresse et ce port. Le programme du webhook ou le proxy est arrêté
Connection timed outUn pare-feu bloque, ou l'adresse est injoignable
Read timeout expiredPas de réponse dans les 30 secondes
Failed to resolve host: Name or service not knownNom de domaine introuvable (DNS)
SSL error {…}Problème de certificat — expiré, auto-signé, nom qui ne correspond pas
Wrong response from the webhook: 502 Bad GatewayRéponse autre que 2xx. Le nombre est le code d'état renvoyé par le webhook
IP address 10.0.0.5 is reservedLe domaine pointe vers une adresse interne (rien n'est envoyé)

Revenir à getUpdates

curl -s "https://pabal.me/bot$BOT_TOKEN/deleteWebhook"
# Pour jeter aussi les mises à jour en attente : deleteWebhook?drop_pending_updates=true

Les mises à jour déjà récupérées par le webhook (celles auxquelles il a répondu 2xx) ne reviennent pas par getUpdates. Seules celles qui n'ont pas encore été livrées continuent d'arriver par getUpdates.

Tester pendant le développement

  • Tunnel : avec un outil qui associe une adresse HTTPS publique au 127.0.0.1:8081 de votre ordinateur (cloudflared, ngrok, etc.), vous pouvez tester même avec un serveur Pabal de production.
  • Votre propre serveur Pabal : si vous avez lancé vous-même un serveur Pabal de développement, activez TELEGRAM_WEBHOOK_ALLOW_LOCAL=true. Ce serveur enverra alors aussi vers des adresses locales comme http://127.0.0.1:8081/…. Ne l'activez jamais sur un serveur de production : n'importe quel bot pourrait alors envoyer des requêtes vers le réseau interne du serveur (base de données, métadonnées cloud).

Liste de contrôle pour la production

  • Un jeton secret est défini et vérifié à chaque requête.
  • Réponse en moins de 30 secondes : les traitements longs partent dans une file, et on répond 200 aussitôt.
  • Les doublons sont filtrés grâce à update_id.
  • Si le webhook s'arrête longtemps, les mises à jour suivantes s'accumulent et sont perdues au redémarrage du serveur : surveillez le programme du webhook (pending_update_count et last_error_date de getWebhookInfo).
  • Si le jeton fuite : /revoke, puis rappeler setWebhook avec le nouveau jeton. Changer aussi le jeton secret.