Dokumentasi pengembang
Bahasa Indonesia

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 memulaiBot bertanya ke serverServer mengirim ke bot
Yang dibutuhkan botHanya koneksi internet keluarAlamat HTTPS publik
Cocok untukKomputer sendiri, masa pengembangan, di balik firewallServer yang selalu menyala, serverless, banyak bot
Memakai keduanya bersamaanTidak bisa — jika webhook terpasang, getUpdates menghasilkan 409 Conflict

Alur webhook

Server Pabal Webhook bot (HTTPS) ① POST update 5 · X-Telegram-Bot-Api-Secret-Token ② 200 OK → 5 dikonfirmasi, lanjut ③ POST update 6 ④ 500 · gagal sambung · >30 dtk → catat last_error jeda 1 dtk → 2 dtk → 4 dtk … maks. 60 dtk kirim ulang 6 (7 menunggu di belakang 6) ⑤ POST update 6 (ulang) ⑥ 200 + {"method": "sendMessage", "chat_id": …, "text": …} metode di respons dijalankan atas nama bot → balasan sampai ke aplikasi pengguna ⑦ POST update 7 …
Update untuk satu bot dikirim satu per satu, sesuai urutan

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 method dan 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

  1. Jalankan program yang akan menerima webhook. Misalkan Anda menjalankan salah satu contoh di bawah di server bot pada 127.0.0.1:8081.
  2. 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
    }
  3. 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)
  4. 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}
  5. 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_count bernilai 0, berarti update diterima dengan baik. last_error_date dan last_error_message adalah catatan kegagalan terakhir, jadi tetap ada meskipun sudah pulih — nilailah dari waktunya.

Parameter setWebhook

ParameterTipeWajibDeskripsi
urlStringYaAlamat webhook. String kosong menghapus webhook (sama dengan deleteWebhook).
secret_tokenStringOpsional1–256 karakter, A-Z a-z 0-9 _ -. Dikirim di setiap permintaan dalam header X-Telegram-Bot-Api-Secret-Token.
allowed_updatesArray of StringOpsionalJenis yang diterima: message, edited_message, callback_query. Jika kosong, semuanya. Jenis yang tidak disebut tidak dikirim, melainkan dibuang.
drop_pending_updatesBooleanOpsionalJika true, semua update yang belum dikirim dibuang sebelum mulai.
max_connectionsIntegerOpsional1–100, bawaan 40. Nilainya diterima dan disimpan, tetapi untuk menjaga urutan Pabal mengirim satu per satu untuk setiap bot.
certificateInputFileTidak didukungSertifikat self-signed tidak diterima (400). Gunakan sertifikat publik.
ip_addressStringDiabaikanDiterima 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

KolomArti
urlAlamat yang diatur. String kosong jika tidak ada webhook
pending_update_countJumlah update yang belum terkirim dan masih menunggu
ip_addressIP alamat tujuan pengiriman terakhir
last_error_date, last_error_messageWaktu (detik Unix) dan alasan kegagalan terakhir. Tidak ada jika belum pernah gagal. Tetap ada meskipun sudah pulih
max_connections, allowed_updatesNilai yang diberikan ke setWebhook
has_custom_certificateSelalu false

Pesan yang muncul di last_error_message

PesanPenyebab
Connection refusedTidak ada yang mendengarkan di alamat dan port itu. Program webhook atau proxy sedang mati
Connection timed outDiblokir firewall, atau alamatnya tidak terjangkau
Read timeout expiredTidak merespons dalam 30 detik
Failed to resolve host: Name or service not knownNama domain tidak ditemukan (DNS)
SSL error {…}Masalah sertifikat — kedaluwarsa, self-signed, atau nama tidak cocok
Wrong response from the webhook: 502 Bad GatewayRespons selain 2xx. Angkanya adalah kode status yang diberikan webhook
IP address 10.0.0.5 is reservedDomain 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:8081 di 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 seperti http://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_count dan last_error_date di getWebhookInfo).
  • Jika token bocor, lakukan /revoke lalu panggil lagi setWebhook dengan token baru. Ganti juga token rahasianya.