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-Token header?
  • How do I handle message, callback_query, or inline_query updates?
  • Why does Telegram keep retrying my webhook, or why is pending_update_count growing?

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 setWebhook without secret_token, the header is not sent at all.
  • Optional defense in depth: Telegram sends from 149.154.160.0/20 and 91.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 fieldTriggered when
messageNew incoming message of any kind (text, photo, sticker, command, ...)
edited_messageA message known to the bot was edited
channel_postNew post in a channel where the bot is a member
callback_queryA user pressed an inline keyboard button
inline_queryA user typed @yourbot ... in any chat
my_chat_memberThe 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

HeaderDescription
X-Telegram-Bot-Api-Secret-TokenThe secret_token you passed to setWebhook, sent as plain text. Only present if you set one
Content-Typeapplication/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


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Oct 28, 2026

View on GitHub →