How to Test a Telegram Bot Webhook
Point your bot at a public capture URL with setWebhook, inspect the raw updates Telegram sends, then forward them to the bot running on your machine.
Updated:
A Telegram bot gets updates in one of two ways: it polls with getUpdates, or Telegram pushes each update to a webhook URL. Webhooks are what you'll use in production, but they're harder to debug because you can't see what Telegram sends. This guide shows how to point a bot at a capture URL, read the raw updates, and then forward them to a bot running locally.
How Telegram webhooks work
You register an HTTPS URL with setWebhook. From then on, Telegram sends a POST with a JSON-serialized Update object to that URL for every new update. If the response status isn't 2xx, Telegram repeats the request and gives up after a reasonable number of attempts.
Two rules to keep in mind:
- Webhook and
getUpdatesare mutually exclusive. While a webhook is set,getUpdateswon't work. CalldeleteWebhookto go back to polling. - Ports. Webhooks are supported only on ports 443, 80, 88 and 8443. The capture URL uses standard HTTPS on 443 with a valid certificate.
Step 1: Create a capture URL
Open Webhook Inspector and create a URL named, for example, tgbot. Any sub-path after it is captured too, so we'll register the bot with a /webhook suffix that matches the local handler later:
https://webhook.voronyuk.com/abcd23efgh/tgbot/webhook
Step 2: Call setWebhook
Replace <BOT_TOKEN> with the token from @BotFather:
curl -X POST "https://api.telegram.org/bot<BOT_TOKEN>/setWebhook" \
-H "Content-Type: application/json" \
-d '{
"url": "https://webhook.voronyuk.com/abcd23efgh/tgbot/webhook",
"secret_token": "my_bot_secret_123",
"allowed_updates": ["message", "callback_query"],
"drop_pending_updates": true
}'
| Parameter | What it does |
|---|---|
url |
HTTPS URL for updates. An empty string removes the webhook |
secret_token |
1–256 characters (A-Z, a-z, 0-9, _, -). Sent back in the X-Telegram-Bot-Api-Secret-Token header of every request |
allowed_updates |
Update types to receive. If omitted, the previous setting is kept |
drop_pending_updates |
true drops updates that are waiting to be delivered |
max_connections |
Simultaneous HTTPS connections, 1–100 (default 40) |
Step 3: Send a message and inspect the update
Send /start to your bot. A POST appears in the inspector right away, carrying the X-Telegram-Bot-Api-Secret-Token header and a body like this:
{
"update_id": 123456789,
"message": {
"message_id": 42,
"from": {
"id": 111111111,
"is_bot": false,
"first_name": "Taras",
"language_code": "uk"
},
"chat": {
"id": 111111111,
"first_name": "Taras",
"type": "private"
},
"date": 1760950000,
"text": "/start",
"entities": [{ "offset": 0, "length": 6, "type": "bot_command" }]
}
}
Try pressing an inline button, editing a message or adding the bot to a group. Each produces a different update type (callback_query, edited_message, my_chat_member), and seeing the exact JSON saves time when writing handlers. Use search in the inspector to find a specific chat.id or text across all captured updates.
Step 4: Check the webhook status
curl "https://api.telegram.org/bot<BOT_TOKEN>/getWebhookInfo"
The result includes url, pending_update_count, last_error_date, last_error_message, max_connections and allowed_updates. If updates aren't arriving, last_error_message usually explains why.
Step 5: Verify the secret token
The secret token isn't a signature; it's a shared value Telegram sends back unchanged. Compare it in constant time:
import hmac
import os
from flask import Flask, abort, request
app = Flask(__name__)
SECRET = os.environ["TG_WEBHOOK_SECRET"]
@app.post("/webhook")
def webhook():
token = request.headers.get("X-Telegram-Bot-Api-Secret-Token", "")
if not hmac.compare_digest(token, SECRET):
abort(403)
update = request.get_json()
if "message" in update:
print(update["message"]["chat"]["id"], update["message"].get("text"))
return "", 200
if __name__ == "__main__":
app.run(port=3000)
The same check in Node.js:
const crypto = require('crypto');
function validSecret(header, secret) {
const a = Buffer.from(header || '');
const b = Buffer.from(secret);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
Step 6: Forward updates to your local bot
Keep the webhook pointed at the capture URL and forward each update to your bot on localhost. In the Forwarding panel, set the target to http://localhost:3000. The captured sub-path is appended to the target, so .../abcd23efgh/tgbot/webhook arrives at http://localhost:3000/webhook, which is exactly the route in the example above. Then start the CLI agent:
WI_TOKEN='<token>' inspector listen https://webhook.voronyuk.com/abcd23efgh/tgbot --to localhost:3000
Copy the exact command with your token from the panel. The agent runs without a browser, reconnects automatically and replays updates it missed. The browser relay works too, as long as your local server allows CORS. See testing webhooks locally for a comparison.
When a handler throws, fix the code and replay the captured update from the Telegram webhook tester instead of sending the message to the bot again.
Step 7: Switch back to polling
curl "https://api.telegram.org/bot<BOT_TOKEN>/deleteWebhook?drop_pending_updates=true"
After this, getUpdates works again.
Related: Slack Events API and Twilio webhooks.
FAQ
How do I see what my Telegram bot receives?
Set the webhook to a capture URL with setWebhook and send the bot a message. The full Update JSON and headers show up live.
Why does getUpdates fail when a webhook is set?
The two methods are mutually exclusive. Call deleteWebhook before polling with getUpdates.
Which ports does a Telegram webhook support?
443, 80, 88 and 8443. The URL must use HTTPS.
What is X-Telegram-Bot-Api-Secret-Token?
It's the secret_token you passed to setWebhook, sent in a header with every update. Compare it with your stored value to make sure requests come from Telegram.
How do I debug a Telegram webhook locally?
Capture updates on a public URL, then forward them to localhost with the CLI agent or browser relay. You can replay any captured update while you fix your handler.