Gareth Wilson Gareth Wilson

Guide to Telegram Webhooks: Features and Best Practices

Published


Telegram webhooks deliver everything that happens to your bot as it happens: a user sends it a message, presses an inline keyboard button, types an inline query, adds the bot to a group or pays for something. If you build a Telegram bot, a webhook replaces the getUpdates polling loop with a push to your own HTTPS endpoint.

This guide covers how a bot's webhook is set, how to authenticate Telegram's requests with the secret token, how Update objects are shaped and routed, the network requirements, and the best practices for running a Telegram bot receiver in production.

What are Telegram webhooks?

A Telegram bot gets updates in one of two ways: by polling with getUpdates, or by registering a webhook URL with the setWebhook method. The two are mutually exclusive. While a webhook is set, getUpdates does not return anything.

With a webhook set, Telegram sends an HTTPS POST to your URL for every update, with a JSON-serialised Update object as the body. Each bot has one webhook URL.

Telegram does not sign the payload. What it offers instead is a secret token: if you pass secret_token to setWebhook, every request carries that exact value in the X-Telegram-Bot-Api-Secret-Token header, and your endpoint compares it with the value it expects. If you set no token, the header is not sent and nothing in the request identifies Telegram.

Telegram webhook features

FeatureDetails
ConfigurationThe setWebhook Bot API method, one URL per bot
BodyJSON Update object: update_id plus one update-type key
AuthenticationOptional secret_token, sent verbatim in X-Telegram-Bot-Api-Secret-Token
Secret format1 to 256 characters: A-Z, a-z, 0-9, _ and -
Payload signatureNone
Filteringallowed_updates list of update types
Concurrencymax_connections, 1 to 100, default 40
RetriesAny response other than 2XY is retried, and Telegram gives up "after a reasonable amount of attempts"
Response windowNot published
Ports443, 80, 88 or 8443, HTTPS only
TLSTLS 1.2 or later. Verified or self-signed certificate (self-signed needs the certificate parameter)
NetworkIPv4 only
Source IPs149.154.160.0/20 and 91.108.4.0/22, which Telegram says may change
StatusgetWebhookInfo: pending update count and the last delivery error

Common events

An Update has no type field. It has update_id and at most one other field, and the name of that field is the update type:

Update fieldArrives when
messageA new message of any kind reaches the bot: text, photo, sticker, command and so on
edited_messageA message the bot knows about is edited
channel_post / edited_channel_postA post is made or edited in a channel the bot is in
callback_queryA user presses an inline keyboard button
inline_query / chosen_inline_resultA user types @yourbot ... in a chat, or picks one of the bot's inline results
my_chat_memberThe bot's own membership in a chat changes, for example when a user blocks it
chat_memberAnother member's status changes in a chat where the bot is an administrator
message_reaction / message_reaction_countReactions to a message change, in a chat where the bot is an administrator
shipping_query / pre_checkout_querySteps in a payment the bot is handling
poll / poll_answerA poll the bot sent changes state, or a user votes in one
chat_join_requestSomeone asks to join a chat where the bot has the can_invite_users right

The Update reference lists more types than this (business messages, chat boosts, paid media purchases and others), and Telegram adds new ones as the Bot API grows. chat_member, message_reaction and message_reaction_count are only sent if you name them in allowed_updates.

This is one of the test updates from Telegram's webhook guide:

{
  "update_id": 10000,
  "message": {
    "date": 1441645532,
    "chat": {
      "last_name": "Test Lastname",
      "id": 1111111,
      "first_name": "Test",
      "username": "Test"
    },
    "message_id": 1365,
    "from": {
      "last_name": "Test Lastname",
      "id": 1111111,
      "first_name": "Test",
      "username": "Test"
    },
    "text": "/start"
  }
}

Telegram wrote these test updates by hand, and they leave out fields that real updates include, such as is_bot on from and type on chat. Build your types from the Bot API reference rather than from the examples.

update_id starts at a positive number and increases by one for each update. Telegram describes it as the way to ignore repeated updates and restore the correct order. If a bot gets no updates for at least a week, the next update_id is chosen at random rather than sequentially.

Setting up Telegram webhooks

  1. Create a bot with @BotFather and keep the bot token server-side. It is the credential for calling the Bot API, not for verifying webhooks.
  2. Generate a random secret token of up to 256 characters using only A-Z, a-z, 0-9, _ and -, and store it server-side, for example as TELEGRAM_WEBHOOK_SECRET.
  3. Call setWebhook with your HTTPS URL, the secret token and the update types you want:
curl -X POST "https://api.telegram.org/bot$TELEGRAM_BOT_TOKEN/setWebhook" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/webhooks/telegram",
    "secret_token": "'"$TELEGRAM_WEBHOOK_SECRET"'",
    "allowed_updates": ["message", "callback_query", "my_chat_member"]
  }'
  1. Call getWebhookInfo to confirm the URL and watch pending_update_count, last_error_date and last_error_message.

Two setWebhook details are easy to miss. If you leave allowed_updates out, Telegram keeps the previous setting, and a change does not apply to updates created before the call, so some unwanted updates can still arrive for a short time. drop_pending_updates: true discards the queue of updates that have not been delivered yet, which is useful when you move a bot to a new URL and don't want a backlog.

To change the secret token, call setWebhook again with the new value. To go back to polling, call deleteWebhook.

Securing Telegram webhooks

Read X-Telegram-Bot-Api-Secret-Token and compare it with your stored token in constant time. Reject the request if the header is missing or different. There is nothing to compute over the body, so you can parse the JSON once the token matches.

const crypto = require('crypto');
const express = require('express');

function verifyTelegramSecretToken(headerValue, expectedSecret) {
  if (!headerValue || !expectedSecret) return false;
  const received = Buffer.from(headerValue, 'utf8');
  const expected = Buffer.from(expectedSecret, 'utf8');
  // timingSafeEqual throws on different lengths, so check first.
  return received.length === expected.length && crypto.timingSafeEqual(received, expected);
}

const app = express();

app.post('/webhooks/telegram', express.raw({ type: '*/*' }), (req, res) => {
  const secret = process.env.TELEGRAM_WEBHOOK_SECRET;
  if (!secret) return res.sendStatus(500); // fail closed

  if (!verifyTelegramSecretToken(req.get('x-telegram-bot-api-secret-token'), secret)) {
    return res.sendStatus(401);
  }

  const update = JSON.parse(req.body.toString('utf8'));
  const type = Object.keys(update).find((key) => key !== 'update_id'); // e.g. "message"
  // Store update.update_id for deduplication, queue the update, then respond.
  res.sendStatus(200);
});

The same check in Python:

import hmac
import os


def verify_telegram_secret_token(header_value, expected: str) -> bool:
    if not header_value or not expected:
        return False
    return hmac.compare_digest(header_value.encode("utf-8"), expected.encode("utf-8"))


# In FastAPI:
# verify_telegram_secret_token(request.headers.get("x-telegram-bot-api-secret-token"),
#                              os.environ["TELEGRAM_WEBHOOK_SECRET"])

Because the token travels on every request, it proves the request came from the webhook you set, but it does not protect the body: anyone who obtains the token can send any update. Keep the endpoint on HTTPS, keep the token out of logs, and, if you want a second layer, allow only Telegram's published ranges at your firewall.

Make Telegram webhooks production-ready. Hookdeck Event Gateway checks the X-Telegram-Bot-Api-Secret-Token header for you, accepts each update straight away, and queues it until your bot has processed it.

Hookdeck's Telegram source type compares X-Telegram-Bot-Api-Secret-Token with the secret token you enter, which is the value you passed to setWebhook. Verification is optional on the source: if you didn't set a secret_token, Telegram sends no header, so leave verification off. Hookdeck's URLs use HTTPS on port 443, which Telegram accepts.

Telegram webhook limitations and pain points

The secret token is optional

The Problem: A bot whose webhook was set without a token accepts requests from anyone who finds the URL.

Why It Happens: secret_token is an optional setWebhook parameter. Without it, Telegram sends no authentication header at all.

Workarounds:

  • Always pass secret_token, and reject requests that lack the header.
  • Restrict the endpoint to 149.154.160.0/20 and 91.108.4.0/22, and recheck Telegram's guide if deliveries stop, because the ranges can change.

How Hookdeck Can Help: With verification on, Hookdeck rejects any request that does not carry the right token before it reaches your bot.

Failed updates pile up

The Problem: After an outage or a bad deploy, the bot falls behind, and pending_update_count keeps growing.

Why It Happens: Telegram retries every response that is not 2XY and gives up only "after a reasonable amount of attempts". It publishes no schedule, no attempt count and no timeout, so you can't predict when an update is dropped.

Workarounds:

  • Return 200 as soon as the update is stored, even if later processing fails.
  • Monitor getWebhookInfo and alert on a rising pending_update_count or a new last_error_message.
  • Use drop_pending_updates when a backlog is no longer worth processing.

How Hookdeck Can Help: Hookdeck accepts each update immediately, so Telegram's queue stays empty, and retries delivery to your bot on a schedule you control, with replay for anything that failed.

Updates arrive concurrently and out of order

The Problem: Two messages from the same user are processed in the wrong order, or the same update is handled twice.

Why It Happens: Telegram opens up to max_connections simultaneous connections (40 by default), and retries can repeat an update.

Workarounds:

  • Deduplicate on update_id.
  • Where order matters, reorder by update_id within a chat, remembering that it can jump to a random value after a week of silence.
  • Lower max_connections if your server can't handle parallel requests.

How Hookdeck Can Help: Hookdeck's deduplication can drop repeated updates based on update_id, and its delivery rate limits control how many updates reach your bot at once.

Replying in the webhook response doesn't work through a gateway

The Problem: A bot that answers by returning a Bot API method in its HTTP response stops replying when you put a gateway in front of it.

Why It Happens: The Bot API lets a webhook response carry a method call, with the method name in a method parameter. Telegram reads that response, and Telegram notes you can't tell whether the call succeeded. With Hookdeck in front, Telegram receives Hookdeck's acknowledgement, not your handler's response.

Workarounds:

  • Call the Bot API explicitly (sendMessage, answerCallbackQuery and so on) from your handler. This also tells you whether the call succeeded.

How Hookdeck Can Help: Once your bot calls the Bot API directly, Hookdeck's queue lets it do so at its own pace instead of inside Telegram's delivery request.

Strict network requirements

The Problem: setWebhook succeeds, but no updates arrive.

Why It Happens: Telegram only delivers over HTTPS with TLS 1.2 or later, on ports 443, 80, 88 or 8443, over IPv4, to a certificate whose CN or SAN matches the domain and whose chain includes all intermediates. A self-signed certificate also has to be uploaded with the certificate parameter.

Workarounds:

  • Check last_error_message in getWebhookInfo, which usually names the problem.
  • Use a certificate from a public CA and serve the full chain.

How Hookdeck Can Help: A Hookdeck source URL already meets Telegram's requirements, so your own server only has to accept connections from Hookdeck.

Best practices

Always set a secret token

Generate a long random token, pass it as secret_token, and compare it in constant time on every request. Treat the bot token and the secret token as separate credentials and store both server-side.

Route on the update key

Find the one key that is not update_id and switch on it. Within message, check which content field is present (text, photo, document and so on). Log and acknowledge any update type you do not handle.

Subscribe only to what you handle

Set allowed_updates explicitly. It keeps traffic down, and it is the only way to receive chat_member and reaction updates.

Deduplicate on update_id

Store each update_id before processing and skip any you have seen. See our guide to webhook idempotency.

Acknowledge fast and process asynchronously

Telegram retries anything that is not a 2XY, and slow responses hold connections open. Store the update, return 200, and do the work afterwards. See why to process webhooks asynchronously.

Treat large IDs carefully

The Bot API notes that some identifiers, such as user and chat IDs, can have more than 32 significant bits. Store them as 64-bit integers or strings.

Conclusion

Telegram does not sign its webhooks. The secret_token you pass to setWebhook comes back verbatim in X-Telegram-Bot-Api-Secret-Token, and checking it in constant time is what tells you a request came from your webhook. Route on the update's one non-update_id key, deduplicate and order on update_id, and watch getWebhookInfo for a growing backlog.

Hookdeck Event Gateway sits in front of your bot, checks the secret token, accepts every update immediately, and holds it until your bot has processed it.

Get started with Hookdeck for free and handle Telegram webhooks reliably in minutes.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.