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'initiative | Le bot interroge le serveur | Le serveur envoie au bot |
| Ce dont le bot a besoin | Un simple accès Internet sortant | Une adresse HTTPS publique |
| Idéal pour | Votre ordinateur, le développement, derrière un pare-feu | Un serveur toujours allumé, du serverless, plusieurs bots |
| Les deux à la fois | Impossible — quand un webhook est configuré, getUpdates renvoie 409 Conflict | |
Comment circule un webhook
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
methodet 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
- 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. - Placez HTTPS devant. Avec Caddy, par exemple, ces deux lignes suffisent, certificat compris.
bot.example.com { reverse_proxy 127.0.0.1:8081 } - 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) - 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} - 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_countvaut 0, tout est bien reçu.last_error_dateetlast_error_messagesont 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ètre | Type | Obligatoire | Description |
|---|---|---|---|
url | String | Oui | Adresse du webhook. Une chaîne vide supprime le webhook (comme deleteWebhook). |
secret_token | String | Facultatif | 1 à 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_updates | Array of String | Facultatif | Types à recevoir : message, edited_message, callback_query. Vide = tous. Les types absents ne sont pas envoyés mais jetés. |
drop_pending_updates | Boolean | Facultatif | Si true, toutes les mises à jour pas encore livrées sont jetées avant de commencer. |
max_connections | Integer | Facultatif | 1 à 100, 40 par défaut. Accepté et conservé, mais pour respecter l'ordre, Pabal n'envoie qu'une requête à la fois par bot. |
certificate | InputFile | Non pris en charge | Les certificats auto-signés ne sont pas acceptés (400). Utilisez un certificat public. |
ip_address | String | Ignoré | 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_idce 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
| Champ | Signification |
|---|---|
url | Adresse configurée. Chaîne vide s'il n'y a pas de webhook |
pending_update_count | Nombre de mises à jour en attente, pas encore livrées |
ip_address | IP de l'adresse vers laquelle le dernier envoi a eu lieu |
last_error_date, last_error_message | Heure (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_updates | Valeurs passées à setWebhook |
has_custom_certificate | Toujours false |
Messages possibles dans last_error_message
| Message | Cause |
|---|---|
Connection refused | Rien n'écoute sur cette adresse et ce port. Le programme du webhook ou le proxy est arrêté |
Connection timed out | Un pare-feu bloque, ou l'adresse est injoignable |
Read timeout expired | Pas de réponse dans les 30 secondes |
Failed to resolve host: Name or service not known | Nom 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 Gateway | Réponse autre que 2xx. Le nombre est le code d'état renvoyé par le webhook |
IP address 10.0.0.5 is reserved | Le 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:8081de 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 commehttp://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_countetlast_error_datedegetWebhookInfo). - Si le jeton fuite :
/revoke, puis rappelersetWebhookavec le nouveau jeton. Changer aussi le jeton secret.