# Guide to Telegram Webhooks: Features and Best Practices

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

| Feature | Details |
| --- | --- |
| Configuration | The `setWebhook` Bot API method, one URL per bot |
| Body | JSON `Update` object: `update_id` plus one update-type key |
| Authentication | Optional `secret_token`, sent verbatim in `X-Telegram-Bot-Api-Secret-Token` |
| Secret format | 1 to 256 characters: `A-Z`, `a-z`, `0-9`, `_` and `-` |
| Payload signature | None |
| Filtering | `allowed_updates` list of update types |
| Concurrency | `max_connections`, 1 to 100, default 40 |
| Retries | Any response other than `2XY` is retried, and Telegram gives up "after a reasonable amount of attempts" |
| Response window | Not published |
| Ports | 443, 80, 88 or 8443, HTTPS only |
| TLS | TLS 1.2 or later. Verified or self-signed certificate (self-signed needs the `certificate` parameter) |
| Network | IPv4 only |
| Source IPs | `149.154.160.0/20` and `91.108.4.0/22`, which Telegram says may change |
| Status | `getWebhookInfo`: 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 field | Arrives when |
| --- | --- |
| `message` | A new message of any kind reaches the bot: text, photo, sticker, command and so on |
| `edited_message` | A message the bot knows about is edited |
| `channel_post` / `edited_channel_post` | A post is made or edited in a channel the bot is in |
| `callback_query` | A user presses an inline keyboard button |
| `inline_query` / `chosen_inline_result` | A user types `@yourbot ...` in a chat, or picks one of the bot's inline results |
| `my_chat_member` | The bot's own membership in a chat changes, for example when a user blocks it |
| `chat_member` | Another member's status changes in a chat where the bot is an administrator |
| `message_reaction` / `message_reaction_count` | Reactions to a message change, in a chat where the bot is an administrator |
| `shipping_query` / `pre_checkout_query` | Steps in a payment the bot is handling |
| `poll` / `poll_answer` | A poll the bot sent changes state, or a user votes in one |
| `chat_join_request` | Someone 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:

```json
{
  "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](https://t.me/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:

```bash
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.

```javascript
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:

```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](/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](/webhooks/guides/implement-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](/webhooks/guides/why-implement-asynchronous-processing-webhooks).

### 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](https://hookdeck.com) 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](https://dashboard.hookdeck.com/signup) for free and handle Telegram webhooks reliably in minutes.