BotNetwork

Setting up a Telegram bot webhook: setWebhook, secret token, debugging

Our earlier post compared webhooks and polling. This one is the hands-on part: getting a Telegram webhook registered, secured and debugged.

Published 4 min read

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 certificate parameter; 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 called deleteWebhook).
  • pending_update_count: growing means deliveries are failing.
  • last_error_date and last_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