Pengembangan bot
Webhook
Cara membuat server langsung mengirim update baru ke alamat HTTPS bot Anda, alih-alih memakai getUpdates — pengaturan, verifikasi, percobaan ulang, dan membalas lewat respons.
getUpdates adalah cara ketika bot terus-menerus bertanya ke server. Webhook adalah kebalikannya: begitu ada update baru, server Pabal langsung mengirimkannya ke alamat HTTPS bot. Jika bot Anda berjalan di server yang selalu menyala (cloud atau server internal), webhook lebih cocok.
| getUpdates (long polling) | Webhook | |
|---|---|---|
| Siapa yang memulai | Bot bertanya ke server | Server mengirim ke bot |
| Yang dibutuhkan bot | Hanya koneksi internet keluar | Alamat HTTPS publik |
| Cocok untuk | Komputer sendiri, masa pengembangan, di balik firewall | Server yang selalu menyala, serverless, banyak bot |
| Memakai keduanya bersamaan | Tidak bisa — jika webhook terpasang, getUpdates menghasilkan 409 Conflict | |
Alur webhook
Penjelasan diagram
- Dua jalur: di kiri server Pabal, di kanan alamat webhook yang Anda jalankan. Garis tegas biru adalah permintaan yang dikirim server, abu-abu adalah respons normal dari webhook, dan garis putus-putus merah adalah kegagalan.
- Respons 2xx berarti "sudah diterima" (②). Sampai saat itu, update tetap tersimpan di server. Badan respons boleh kosong.
- Jika gagal, update yang sama dikirim ulang (④ → ⑤): respons selain 2xx, koneksi gagal, dan tidak ada jawaban dalam 30 detik semuanya dianggap gagal; jedanya mulai dari 1 detik dan berlipat dua hingga maksimal 60 detik. Alasan dan waktu kegagalan tercatat di
getWebhookInfo. - Urutan dijaga: update untuk satu bot dikirim satu per satu, jadi 7 menunggu sampai 6 berhasil. Karena itu, jika webhook gagal dalam waktu lama, update berikutnya akan menumpuk (
pending_update_count). - Anda bisa membalas lewat respons (⑥): jika badan respons 200 memuat
methoddan parameternya, server menjalankan metode itu atas nama bot. Tidak perlu mengirim permintaan sekali lagi, jadi lebih cepat.
Syarat alamat webhook
- Harus berupa alamat https://. Sertifikatnya harus diterbitkan oleh otoritas sertifikat publik (Let's Encrypt dan sebagainya); mengunggah sertifikat self-signed (
certificate) tidak didukung. - Harus berupa alamat publik yang bisa dijangkau dari internet. Server tidak mengirim ke alamat internal seperti loopback (
127.0.0.1,::1), jaringan privat (10.,172.16–31.,192.168.), link-local (169.254., metadata cloud), atau CGNAT (100.64/10). Domain yang mengarah ke alamat seperti itu juga ditolak, dan pemeriksaan diulang setiap kali mengirim. - Tidak ada batasan port (tidak harus 443). Nama pengguna dan kata sandi (
https://user:pw@…) tidak boleh dimasukkan ke alamat. - Pengalihan (3xx) tidak diikuti dan dianggap gagal. Masukkan alamat akhirnya.
- Harus merespons dalam 30 detik. Untuk pekerjaan yang lama, jawab dulu dengan 200 lalu proses di belakang.
Mengatur webhook
- Jalankan program yang akan menerima webhook. Misalkan Anda menjalankan salah satu contoh di bawah di server bot pada
127.0.0.1:8081. - Pasang HTTPS di depannya. Misalnya dengan Caddy, dua baris ini sudah cukup, termasuk sertifikatnya secara otomatis.
bot.example.com { reverse_proxy 127.0.0.1:8081 } - Buat sebuah token rahasia. 1–256 karakter berupa huruf Latin, angka,
_, dan-. Server memasukkan nilai ini ke header setiap permintaan, sehingga Anda bisa memastikan permintaan benar-benar datang dari server Pabal.export WEBHOOK_SECRET=$(openssl rand -hex 32) - Panggil 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} - Periksa statusnya. Kirim pesan ke bot dari aplikasi, lalu lihat
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"] } }Jika
pending_update_countbernilai 0, berarti update diterima dengan baik.last_error_datedanlast_error_messageadalah catatan kegagalan terakhir, jadi tetap ada meskipun sudah pulih — nilailah dari waktunya.
Parameter setWebhook
| Parameter | Tipe | Wajib | Deskripsi |
|---|---|---|---|
url | String | Ya | Alamat webhook. String kosong menghapus webhook (sama dengan deleteWebhook). |
secret_token | String | Opsional | 1–256 karakter, A-Z a-z 0-9 _ -. Dikirim di setiap permintaan dalam header X-Telegram-Bot-Api-Secret-Token. |
allowed_updates | Array of String | Opsional | Jenis yang diterima: message, edited_message, callback_query. Jika kosong, semuanya. Jenis yang tidak disebut tidak dikirim, melainkan dibuang. |
drop_pending_updates | Boolean | Opsional | Jika true, semua update yang belum dikirim dibuang sebelum mulai. |
max_connections | Integer | Opsional | 1–100, bawaan 40. Nilainya diterima dan disimpan, tetapi untuk menjaga urutan Pabal mengirim satu per satu untuk setiap bot. |
certificate | InputFile | Tidak didukung | Sertifikat self-signed tidak diterima (400). Gunakan sertifikat publik. |
ip_address | String | Diabaikan | Diterima tetapi tidak dipakai. Server selalu mencari alamat domain saat itu juga. |
Pengaturan webhook disimpan di server, sehingga tetap berlaku meskipun server dimulai ulang, dan pengiriman langsung dilanjutkan begitu server menyala. Namun update yang belum sempat dikirim ada di memori, jadi hilang saat server dimulai ulang.
Permintaan yang dikirim server
POST /pabal-webhook HTTP/1.1
Host: bot.example.com
Content-Type: application/json
X-Telegram-Bot-Api-Secret-Token: 3f1c…(nilai yang diberikan ke setWebhook)
{"update_id":12,"message":{"message_id":3,"from":{"id":100001,"is_bot":false,"first_name":"Hana"},"chat":{"id":100001,"first_name":"Hana","type":"private"},"date":1789805661,"text":"Hai"}}
- Badannya adalah objek Update, sama persis dengan satu elemen hasil
getUpdates. - Pastikan Anda memeriksa token rahasia. Jika header tidak ada atau nilainya berbeda, tolak dengan 401. Bandingkan dengan fungsi yang waktunya konstan (
hmac.compare_digest,crypto.timingSafeEqual). - Update yang sama bisa datang dua kali (jika koneksi terputus sebelum respons sampai ke server). Aman jika Anda melewati update yang sudah diproses berdasarkan
update_id.
Membalas lewat respons
Jika badan respons 200 memuat satu panggilan Bot API, server menjalankan panggilan itu atas nama bot. Masukkan nama metode ke method, bersama parameter lainnya.
{"method": "sendMessage", "chat_id": 100001, "text": "Halo!"}
- Format badan bisa JSON, formulir, atau
multipart/form-data(aiogram mengirim dalam multipart). Ukurannya hingga 1MB. - Hasil atau error dari panggilan ini tidak dikembalikan ke bot (hanya tercatat di log server). Jika Anda butuh hasilnya, kirim permintaan tersendiri seperti biasa.
- Jika tidak ada yang perlu dibalas, cukup kembalikan 200 tanpa badan.
Contoh
Keempat contoh ini memeriksa token rahasia, dan saat menerima pesan teks, menirukannya dengan memasukkan sendMessage ke dalam respons. Diasumsikan ada HTTPS di depannya (misalnya Caddy) dan program menerima di 127.0.0.1:8081.
# webhook.py — hanya pustaka standar
# Jalankan: 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 ke atas, tanpa pustaka
// Jalankan: 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
# Program ini juga yang mengatur webhook-nya (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: dikirim di dalam respons 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]"
# Program ini juga yang mengatur webhook-nya (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"])Melihat status — getWebhookInfo
| Kolom | Arti |
|---|---|
url | Alamat yang diatur. String kosong jika tidak ada webhook |
pending_update_count | Jumlah update yang belum terkirim dan masih menunggu |
ip_address | IP alamat tujuan pengiriman terakhir |
last_error_date, last_error_message | Waktu (detik Unix) dan alasan kegagalan terakhir. Tidak ada jika belum pernah gagal. Tetap ada meskipun sudah pulih |
max_connections, allowed_updates | Nilai yang diberikan ke setWebhook |
has_custom_certificate | Selalu false |
Pesan yang muncul di last_error_message
| Pesan | Penyebab |
|---|---|
Connection refused | Tidak ada yang mendengarkan di alamat dan port itu. Program webhook atau proxy sedang mati |
Connection timed out | Diblokir firewall, atau alamatnya tidak terjangkau |
Read timeout expired | Tidak merespons dalam 30 detik |
Failed to resolve host: Name or service not known | Nama domain tidak ditemukan (DNS) |
SSL error {…} | Masalah sertifikat — kedaluwarsa, self-signed, atau nama tidak cocok |
Wrong response from the webhook: 502 Bad Gateway | Respons selain 2xx. Angkanya adalah kode status yang diberikan webhook |
IP address 10.0.0.5 is reserved | Domain mengarah ke alamat internal (tidak dikirim) |
Kembali ke getUpdates
curl -s "https://pabal.me/bot$BOT_TOKEN/deleteWebhook"
# Untuk sekaligus membuang update yang sedang menunggu: deleteWebhook?drop_pending_updates=true
Update yang sudah diambil oleh webhook (dijawab dengan 2xx) tidak akan datang lagi lewat getUpdates. Hanya update yang belum terkirim yang dilanjutkan lewat getUpdates.
Menguji selama pengembangan
- Tunnel: dengan alat yang memberi alamat HTTPS publik ke
127.0.0.1:8081di komputer Anda (cloudflared, ngrok, dan sebagainya), Anda juga bisa menguji dengan server Pabal produksi. - Server Pabal Anda sendiri: jika Anda menjalankan sendiri server Pabal untuk pengembangan, aktifkan dengan
TELEGRAM_WEBHOOK_ALLOW_LOCAL=true. Server itu juga akan mengirim ke alamat lokal sepertihttp://127.0.0.1:8081/…. Jangan pernah mengaktifkannya di server produksi — satu bot saja bisa mengirim permintaan ke jaringan internal server (basis data, metadata cloud).
Daftar periksa operasional
- Token rahasia sudah ditetapkan dan diperiksa di setiap permintaan.
- Menjawab dalam 30 detik — pekerjaan yang lama dilempar ke antrean dan langsung dijawab 200.
- Duplikat disaring berdasarkan
update_id. - Jika webhook berhenti lama, update berikutnya menumpuk dan hilang saat server dimulai ulang — pantau program webhook Anda (
pending_update_countdanlast_error_datedigetWebhookInfo). - Jika token bocor, lakukan
/revokelalu panggil lagisetWebhookdengan token baru. Ganti juga token rahasianya.