Telegram Bot Developers
Debug every Telegram bot updatefrom setWebhook to production
The Telegram Bot API only delivers updates to a public HTTPS URL, so localhost bots stay blind. Hooklistener captures every update, tunnels them to your machine, and alerts you when updates stop arriving.
How Telegram webhooks work
Telegram delivers each bot update as an HTTPS POST with a JSON-serialized Update object to the one URL you register with setWebhook. This reference covers the parameters, response fields and error strings you will meet while setting that URL up, checking it and testing it locally.
| Allowed webhook ports | 443, 80, 88 and 8443. Other ports are not supported, and the URL must be HTTPS (TLS 1.2 or newer) whichever port you use. |
|---|---|
| Secret header | X-Telegram-Bot-Api-Secret-Token, sent on every request when you pass secret_token to setWebhook. The token is 1-256 characters; only A-Z, a-z, 0-9, _ and - are allowed. |
| getUpdates vs webhook | The two are mutually exclusive. While a webhook is set, getUpdates fails with 409 Conflict; call deleteWebhook first to go back to polling. |
| max_connections | Simultaneous HTTPS connections Telegram opens to your webhook: 1-100, default 40. |
| Retries | For any response status other than 2XY, Telegram will "repeat the request and give up after a reasonable amount of attempts". Undelivered updates are kept on Telegram's servers for no longer than 24 hours. |
| allowed_updates default | An empty list means every update type except chat_member, message_reaction and message_reaction_count. If you omit the parameter, the previous setting is kept. |
Set a Telegram webhook (setWebhook)
Call setWebhook with the HTTPS URL that should receive updates. secret_token adds the verification header, allowed_updates limits the update types (send it as a JSON-serialized list), and drop_pending_updates=true discards whatever is queued from before. If the URL uses a port other than 443, put the port in the URL itself, for example https://bot.example.com:8443/telegram.
curl -X POST "https://api.telegram.org/bot<TOKEN>/setWebhook" \
-H "Content-Type: application/json" \
-d '{
"url": "https://bot.example.com/telegram",
"secret_token": "my_secret_123",
"allowed_updates": ["message", "callback_query"],
"drop_pending_updates": true
}'Check it with getWebhookInfo
getWebhookInfo takes no parameters and returns a WebhookInfo object. url is empty when no webhook is set (or the bot is using getUpdates). pending_update_count is the number of updates waiting for delivery. last_error_date is the Unix time of the most recent delivery error and last_error_message describes it in readable form. The response also echoes max_connections, allowed_updates and has_custom_certificate, so you can confirm what Telegram actually stored.
curl "https://api.telegram.org/bot<TOKEN>/getWebhookInfo"
# Example response shape
{
"ok": true,
"result": {
"url": "https://bot.example.com/telegram",
"has_custom_certificate": false,
"pending_update_count": 3,
"last_error_date": 1762819200,
"last_error_message": "Wrong response from the webhook: 502 Bad Gateway",
"max_connections": 40
}
}Remove it (deleteWebhook)
deleteWebhook removes the webhook so the bot can switch back to getUpdates. Pass drop_pending_updates=true to discard the queued updates at the same time. Calling setWebhook with an empty url does the same removal.
curl -X POST "https://api.telegram.org/bot<TOKEN>/deleteWebhook" \
-d "drop_pending_updates=true"Verify X-Telegram-Bot-Api-Secret-Token
Telegram does not sign the body. The secret token you chose comes back verbatim in a header, so compare it with the stored value before you touch the update and reject anything that does not match. Answer quickly with a 2XX status, because any other status makes Telegram send the update again.
app.post("/telegram", express.json(), (req, res) => {
if (req.get("X-Telegram-Bot-Api-Secret-Token") !== process.env.TELEGRAM_SECRET) {
return res.sendStatus(401);
}
queueUpdate(req.body); // process the Update outside the request
res.sendStatus(200);
});Ports and TLS
Telegram connects over IPv4 only and needs TLS 1.2 or newer. The certificate's CN or SAN must match the webhook domain, and your server must send every intermediate certificate in the chain. If you use a self-signed certificate, upload its public PEM file in the certificate parameter as multipart/form-data (InputFile). Sending it as a string does not work. If your CA's root is not trusted by Telegram, upload that root certificate the same way. The port rule applies to the public URL you give setWebhook, not to the port your local app listens on.
curl -F "url=https://203.0.113.10:8443/telegram" \
-F "certificate=@public.pem" \
"https://api.telegram.org/bot<TOKEN>/setWebhook"Test a Telegram webhook locally
Run your bot on localhost, open a tunnel with the Hooklistener CLI, point setWebhook at the HTTPS URL the CLI prints, then send your bot a message. Each update then reaches your local handler. If the printed URL changes when you restart the tunnel, call setWebhook again with the new one. Updates captured at a Hooklistener webhook URL can be sent to your handler again with replay (Pro).
hooklistener login
hooklistener tunnel --port 3000
# In another terminal, use the URL the tunnel printed
curl -X POST "https://api.telegram.org/bot<TOKEN>/setWebhook" \
-d "url=https://<printed-tunnel-url>/telegram" \
-d "secret_token=my_secret_123"
# Now send your bot a message in TelegramRequest headers
| Header | Meaning |
|---|---|
| X-Telegram-Bot-Api-Secret-Token | The secret_token value from setWebhook, sent as-is on every webhook request. Absent if you never set one. |
| Content-Type: application/json | The body is a single JSON-serialized Update object. |
Common errors
| Symptom | Cause | Fix |
|---|---|---|
| last_error_message reports an SSL error such as "certificate verify failed" | Telegram could not build a trusted chain to your certificate: a missing intermediate certificate, a CN/SAN that does not match the domain, or a self-signed or untrusted-root certificate that was never uploaded. | Serve the full chain (server certificate followed by intermediates), make the CN or SAN match the webhook host, or re-run setWebhook with the public PEM in the certificate parameter. |
| "Wrong response from the webhook: 502 Bad Gateway" (or 504 Gateway Timeout) | Telegram reached a proxy, load balancer or tunnel, but it could not get a response from your app (502) or gave up waiting for one (504). Usually the app is down, listening on a different port, or doing slow work before it answers. | Confirm the app is running on the port the proxy or tunnel forwards to, and return 200 right away while the update is processed in the background. |
| pending_update_count keeps growing | Deliveries are failing, so updates queue on Telegram's side. They are kept for no longer than 24 hours. | Read last_error_message, fix the endpoint so it returns 2XX, and if the backlog is stale, clear it with drop_pending_updates=true on setWebhook or deleteWebhook. |
| 409 Conflict: terminated by other getUpdates request | Two copies of the bot are long-polling getUpdates with the same token. A related 409 says getUpdates cannot be used while a webhook is active. | Run a single bot instance per token. To poll instead of using a webhook, call deleteWebhook first; to use a webhook, stop every getUpdates loop. |
Sources (checked November 2026)
Make sense of Telegram's webhook-first Bot API
Telegram pushes every message, callback_query, and inline_query to the single URL you register with setWebhook, and that URL must be public HTTPS with a valid certificate. When delivery fails, Telegram retries quietly while pending_update_count climbs and getWebhookInfo only shows the last error.
Point setWebhook at a Hooklistener endpoint to see exactly what Telegram sends: full update payloads, the X-Telegram-Bot-Api-Secret-Token header, and delivery timing. Then tunnel updates to localhost while you get your handler right.
Why Telegram webhooks break
Localhost is unreachable
setWebhook rejects anything that isn't public HTTPS, so you can't point a bot at your dev machine. The Hooklistener CLI tunnel forwards live updates to localhost from a random hook.events subdomain on Free, and a static subdomain that survives restarts on Pro.
Silent delivery failures
Telegram retries failed deliveries without telling you, and getWebhookInfo only surfaces the last error message. Hooklistener logs every request so you see exactly when and why updates stopped flowing.
Mystery payload shapes
A message update looks nothing like a callback_query or pre_checkout_query. Capture real updates of every type and inspect prettified JSON instead of guessing from the docs.
The Hooklistener workflow for Telegram bots
Capture every Telegram update
Register a Hooklistener endpoint with Telegram's setWebhook and record every update with headers, body and timing, so a message and a callback_query can be compared field by field. Use replay (Pro) to resend an update to your handler without messaging the bot again.
CLI tunnels for local dev
Telegram can't reach localhost, so forward live updates to your machine over an outbound WebSocket that works behind firewalls. The Tunnel Traffic Inspector streams each request in real time with method, path, status, and duration.
Alerts when updates stop
Telegram retries quietly, so attach an inactivity monitor to your bot's endpoint and get an email, Slack, or signed-webhook alert when Telegram goes quiet for longer than the window you set, from 1 minute up to 24 hours.
Try it: point Telegram at a free webhook URL or run a test with the webhook tester.
Telegram bot flows worth instrumenting
Command and conversation handlers
Capture real message updates to verify entities, chat IDs, and reply logic before your bot answers users in production.
Inline keyboards and callbacks
Debug callback_query payloads from inline buttons and confirm you answer them fast enough to clear the loading spinner.
Channels, groups, and payments
Inspect channel_post, chat_member, and pre_checkout_query updates so moderation tools and payment flows never miss an event.
Replay a failed webhook without re-triggering the provider
Once a delivery is captured, you can resend its exact headers and body to localhost, staging or production from the dashboard or through your coding agent over MCP, so the provider never has to fire a new event. Replay is on Pro and above. Read the replay guide.
Comparing webhook debugging tools?
See how Hooklistener stacks up against webhook.site, RequestBin, Beeceptor, Hookdeck and ngrok in the best tools to capture and inspect webhook payloads, or read the head-to-head pages for Hooklistener vs ngrok and Hooklistener vs Beeceptor.
Frequently asked questions
Why is my Telegram webhook not working?
Start with getWebhookInfo: check last_error_message, last_error_date, and pending_update_count. Common culprits are invalid certificates, non-HTTPS URLs, unsupported ports (Telegram only delivers to 443, 80, 88, and 8443), and handlers that respond too slowly. Point setWebhook at a Hooklistener endpoint to confirm whether Telegram is delivering at all.
How do I test a Telegram bot webhook locally?
Telegram can't reach localhost, so you need a tunnel. Run hooklistener tunnel --port 3000 to get an HTTPS URL that setWebhook accepts: a random hook.events subdomain on Free, and a static subdomain that survives restarts on Pro. Login uses a device code and the tunnel is an outbound WebSocket connection that works behind firewalls, so no inbound ports are required.
Does Hooklistener show the Telegram secret token?
Yes. If you set secret_token in setWebhook, Telegram sends it as the X-Telegram-Bot-Api-Secret-Token header on every update. Hooklistener captures all headers so you can verify the token arrives and your validation logic matches it.
Can Hooklistener alert me when my bot stops receiving updates?
Yes. Attach an inactivity monitor to the endpoint with a timeout between 1 minute and 24 hours, and Hooklistener alerts you by email, Slack, or signed webhook when no update arrives in that window, with a cooldown so a long outage doesn't spam you.
Stop flying blind on Telegram updates
Capture every update, tunnel them to localhost, and inspect the tricky ones until your bot handles them all. Free to start, no credit card required.