Telegraft

Bot API reference

Proving an update came from Telegram

`setWebhook` accepts an optional `secret_token` of 1–256 characters from `A-Z`, `a-z`, `0-9`, `_` and `-`. Telegram then sends that value in an `X-Telegram-Bot-Api-Secret-Token` header on every webhook request, and comparing it is what distinguishes a real update from one anybody posted at your URL.

X-Telegram-Bot-Api-Secret-Token: the exact figures

secret_token format
1–256 characters from A-Z, a-z, 0-9, _ and -
Header
X-Telegram-Bot-Api-Secret-Token
Length
1–256 characters
Alphabet
A-Z, a-z, 0-9, underscore, hyphen
Rejection response
404, so an unauthenticated caller learns nothing
Rotation
Call setWebhook again; the URL does not change

As of 2025-10-01, Telegram Bot API 13.4

What it means in practice

A webhook endpoint is a public HTTPS URL that accepts JSON. Without validation it accepts that JSON from anyone who knows or guesses the address, and the payload format is fully documented — so a forged update naming any `chat.id` and any `from.id` is trivial to construct. For a bot that only echoes text this is a nuisance; for a bot that grants channel access, credits a balance or marks an order paid, it is the entire security model missing.

Obscurity is the trap here, because it looks like a solution. A URL containing the bot token is a common pattern and it is genuinely better than nothing, but URLs end up in proxy logs, in CDN dashboards, in error reports and in the browser history of whoever tested it. The secret token travels in a header instead, where it is not logged by default and can be rotated without changing the registered URL.

The comparison itself should be constant-time. A naive `===` on strings leaks length and prefix information through timing, and while exploiting that against a webhook is difficult, the mitigation costs one function call. What matters more in practice is failing correctly: the response to a bad token should be a 404 rather than a 401 or 403, because an unauthenticated caller learning that the route exists is the first useful thing an attacker gets.

Rotation is straightforward and worth building in from the start. Call `setWebhook` again with a new `secret_token`; the change takes effect for subsequent requests. Because it is a header rather than part of the URL, nothing else in the deployment has to change — which is precisely the property that makes rotating it a routine operation rather than a migration.

One deployment detail is easy to miss. The secret is chosen by you, not issued by Telegram, so it must be generated with a cryptographic random source and stored as a secret rather than in the repository. A memorable value, or one derived from the bot token, defeats the purpose in the same way as an obscure URL.

Handling it in code

// Constant-time comparison, and a 404 rather than a 401 on failure.
function constantTimeEquals(a: string, b: string): boolean {
  if (a.length !== b.length) return false
  let diff = 0
  for (let i = 0; i < a.length; i += 1) diff |= a.charCodeAt(i) ^ b.charCodeAt(i)
  return diff === 0
}

function isFromTelegram(request: Request, expected: string): boolean {
  const supplied = request.headers.get('X-Telegram-Bot-Api-Secret-Token') ?? ''
  // An empty expected value must never pass: a misconfigured deploy should fail
  // closed rather than silently accepting every forged update.
  return expected !== '' && constantTimeEquals(supplied, expected)
}
The guard on an empty expected value matters as much as the comparison. A deployment that forgot to set the secret should reject everything, not accept everything.

Questions this raises

Is putting the bot token in the webhook URL good enough?

It is better than nothing and worse than the header. URLs leak into proxy logs, CDN dashboards, error trackers and browser history in ways headers do not, and rotating a URL means re-registering the webhook rather than changing one secret. Use both if you like; rely on the header.

What can someone actually do with an unvalidated endpoint?

Post any update the API documents, naming any user and any chat. Against a bot that grants access to a paid channel, credits a referral balance or marks an order as paid, that is a complete authorisation bypass with no exploitation skill required — the payload format is published.

Should I also check the source IP?

It can be a useful second layer, since Telegram publishes its address ranges, but it is a poor primary control: the ranges change, and any misconfiguration in front of your service can rewrite the apparent source. The secret token is the mechanism designed for this and does not depend on network topology.

Does the secret token protect the payload contents?

No. It authenticates the sender, not the message. It proves the request came from something holding the shared secret; it does not sign the body. That distinction matters when comparing it to Mini App init data, which is genuinely signed and can therefore be validated field by field.

Related limits