Before you start
If you are still deciding between the two delivery modes, read webhooks vs polling first. For a webhook, the Bot API requires:
- A public HTTPS URL. Telegram supports ports 443, 80, 88 and 8443 only.
- A valid certificate. A self-signed one works if you upload it with the
certificateparameter; most hosts (Railway, Cloudflare Workers, a VPS behind Caddy) give you a trusted certificate anyway. - A handler that answers with HTTP 2xx quickly. Telegram treats any other status as a failure and retries.
While a webhook is set, getUpdates does not work. You must use one mode or the other.
1. Pick the URL and a secret
Use a path that is not guessable and generate a separate secret value:
export TOKEN="123456:ABC..." # from @BotFather, keep it out of git
export HOOK_URL="https://bot.example.com/telegram/webhook"
export HOOK_SECRET="$(openssl rand -hex 32)"
The secret_token may be 1-256 characters using A-Z, a-z, 0-9, _ and -. A 64-character hex string fits.
2. Call setWebhook
curl -s "https://api.telegram.org/bot$TOKEN/setWebhook" \
-H "Content-Type: application/json" \
-d "{
\"url\": \"$HOOK_URL\",
\"secret_token\": \"$HOOK_SECRET\",
\"allowed_updates\": [\"message\", \"callback_query\"],
\"drop_pending_updates\": true,
\"max_connections\": 40
}"
What each parameter does:
| Parameter | Purpose |
|---|---|
url |
Where Telegram POSTs updates. An empty string removes the webhook. |
secret_token |
Sent back in the X-Telegram-Bot-Api-Secret-Token header of every request. |
allowed_updates |
Only these update types are delivered. Omit it to keep the previous setting. |
drop_pending_updates |
Discards the queue accumulated while the bot was down. |
max_connections |
Simultaneous HTTPS connections, 1-100 (default 40). Lower it for a small server. |
ip_address |
Fixed IP to use instead of DNS resolution. |
The Telegram URL builder composes these calls for you, and the webhook sender can test them.
3. Verify the secret in your handler
Anyone who learns the URL can POST fake updates. Compare the header with your secret and reject mismatches before doing any work:
// Cloudflare Worker or any fetch-style runtime
export default {
async fetch(req, env) {
if (req.method !== "POST") return new Response("ok");
if (req.headers.get("X-Telegram-Bot-Api-Secret-Token") !== env.HOOK_SECRET) {
return new Response("forbidden", { status: 401 });
}
const update = await req.json();
// hand off slow work (queue, waitUntil) and answer fast
return new Response("ok");
},
};
Frameworks do this for you if you pass the secret: grammY's webhookCallback(bot, "cloudflare-mod", { secretToken }), python-telegram-bot's run_webhook(secret_token=...), aiogram's SimpleRequestHandler(secret_token=...).
4. Check getWebhookInfo
curl -s "https://api.telegram.org/bot$TOKEN/getWebhookInfo"
The fields that matter when debugging:
url: empty means no webhook is set (or someone calleddeleteWebhook).pending_update_count: growing means deliveries are failing.last_error_dateandlast_error_message: the exact reason for the last failure.allowed_updates: confirm the types you expect.
Common last_error_message values and fixes:
| Message (abbreviated) | Likely cause |
|---|---|
Wrong response from the webhook: 401 / 403 |
Your secret check rejects Telegram; header name or value mismatch. |
Wrong response from the webhook: 404 |
Path differs from the route your app serves. |
Wrong response from the webhook: 500 / 502 |
Your handler crashed or the app is down. Check logs. |
SSL error ... / certificate verify failed |
Incomplete certificate chain or self-signed cert not uploaded. |
Connection timed out / Read timeout expired |
Handler too slow or a firewall blocks Telegram. |
To see exactly what Telegram sends, point a test bot at a temporary URL from our webhook inspector and look at the headers and body.
5. Answer fast, work later
Telegram keeps retrying failed deliveries, so a handler that takes too long may receive the same update again. Return 200 quickly and move slow work to a queue or background task. Deduplicate by update_id if your processing has side effects.
A useful shortcut: you can reply directly in the webhook response body by returning JSON with a method field, for example {"method":"sendMessage","chat_id":123,"text":"Hi"}. You do not get the result of that call, so use it only for simple fire-and-forget replies.
Going back to polling
curl -s "https://api.telegram.org/bot$TOKEN/deleteWebhook?drop_pending_updates=true"
Without deleteWebhook, a local bot running getUpdates gets a 409 Conflict error.
Frequently asked questions
Why does my bot get the same message twice?
Your endpoint returned a non-2xx status or timed out, so Telegram retried. Answer immediately and process in the background.
Can I run two bot instances on one webhook?
Yes, behind a load balancer, because each update is a separate HTTP request. That is one of the advantages over long polling, where only one getUpdates consumer may run.
How long does Telegram keep updates if my server is down?
Unprocessed updates are kept on Telegram's side until delivered, but not longer than 24 hours, according to the Bot API documentation.
Sources
- Telegram Bot API, setWebhook and getWebhookInfo: core.telegram.org/bots/api#setwebhook
- Telegram, Marvin's Marvellous Guide to All Things Webhook: core.telegram.org/bots/webhooks
- Telegram Bot API, making requests when getting updates: core.telegram.org/bots/api#making-requests-when-getting-updates
- grammY, deployment types: grammy.dev/guide/deployment-types