Agent skill
Telegram Webhooks Skill
Receive and verify Telegram Bot API webhooks (setWebhook). Use when setting up a Telegram bot webhook handler, verifying the X-Telegram-Bot-Api-Secret-Token header, debugging rejected or retried Telegram updates, or handling Update types like message, edited_message, channel_post, callback_query, inline_query, and my_chat_member.
Install this skill
npx skills add hookdeck/webhook-skills --skill telegram-webhooks
When to Use This Skill
- How do I receive Telegram bot updates with a webhook instead of
getUpdates? - How do I verify that a Telegram webhook request really came from Telegram?
- How do I check the
X-Telegram-Bot-Api-Secret-Tokenheader? - How do I handle
message,callback_query, orinline_queryupdates? - Why does Telegram keep retrying my webhook, or why is
pending_update_countgrowing?
How Telegram Webhook Verification Works
Telegram does not sign webhook payloads. There is no HMAC and no signature header. Instead, you pass a secret_token (1-256 characters, only A-Z, a-z, 0-9, _ and -) when you call setWebhook. Telegram then sends that exact value in the X-Telegram-Bot-Api-Secret-Token header on every webhook request. Your handler compares it to the expected value with a constant-time comparison and rejects anything that doesn't match.
- The token covers the request as a whole, not the body, so there is nothing to compute over the raw body. Parse the JSON only after the token check passes.
- If you call
setWebhookwithoutsecret_token, the header is not sent at all. - Optional defense in depth: Telegram sends from
149.154.160.0/20and91.108.4.0/22.
Verification (core)
const crypto = require('crypto');
// headerValue: req.headers['x-telegram-bot-api-secret-token']
function verifyTelegramSecretToken(headerValue, expectedSecret) {
if (!headerValue || !expectedSecret) return false;
const received = Buffer.from(headerValue);
const expected = Buffer.from(expectedSecret);
// timingSafeEqual throws on length mismatch, so check length first
return received.length === expected.length && crypto.timingSafeEqual(received, expected);
}
import hmac
def verify_telegram_secret_token(header_value: str | None, expected: str) -> bool:
if not header_value or not expected:
return False
return hmac.compare_digest(header_value.encode(), expected.encode())
For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.
Common Update Types
Each webhook body is one JSON Update object. There is no type field: an update has update_id plus at most one of the optional fields below, and you route on which one is present.
| Update field | Triggered when |
|---|---|
message | New incoming message of any kind (text, photo, sticker, command, ...) |
edited_message | A message known to the bot was edited |
channel_post | New post in a channel where the bot is a member |
callback_query | A user pressed an inline keyboard button |
inline_query | A user typed @yourbot ... in any chat |
my_chat_member | The bot's own membership changed (added, removed, blocked, unblocked) |
For the full list (
chat_member,message_reaction,pre_checkout_query,poll_answer, ...), see references/overview.md.
Important Headers
| Header | Description |
|---|---|
X-Telegram-Bot-Api-Secret-Token | The secret_token you passed to setWebhook, sent as plain text. Only present if you set one |
Content-Type | application/json |
Environment Variables
TELEGRAM_WEBHOOK_SECRET=your_secret_token # The secret_token passed to setWebhook
TELEGRAM_BOT_TOKEN=123456:ABC-DEF... # Only needed to call setWebhook / the Bot API
Local Development
# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 telegram --path /webhooks/telegram
Then point the bot at the public URL (see references/setup.md). Telegram only delivers to HTTPS URLs on ports 443, 80, 88 or 8443.
Reference Materials
- references/overview.md - Telegram webhook concepts, Update types, payload structure
- references/setup.md - Create a bot, call
setWebhook, checkgetWebhookInfo - references/verification.md - Secret token verification, IP allowlist, gotchas