Everything inbound from Telegram, in one place: understanding what a webhook Update actually contains, and proving that a Mini App's initData or a Login Widget callback really came from Telegram. The two signature schemes share one verified HMAC core here, because their only difference is how the secret key is derived. All of it runs in your browser — bot tokens never leave the page.
{{ updateError }}
Paste the raw body your webhook endpoint received, or an update from getUpdates.
This POST comes from the browser, so it is subject to CORS. Your endpoint will receive the request either way, but the browser can only show you the response if the endpoint sends permissive CORS headers — a network error here does not mean delivery failed. For a throwaway target that always allows it, use webhook.site.
No updates yet. Replay one above and it will be kept here on this device.
The raw value of window.Telegram.WebApp.initData, exactly as received — do not re-encode it.
Builds a correctly signed initData string for local tests and fixtures. It is only valid against the bot token above — it grants nothing on real Telegram infrastructure.
{{ signIssue }}
Paste whichever form you have: the raw callback query string, the full redirect URL, or the JSON object your data-onauth handler received. Detection rule — input starting with { is parsed as JSON, input starting with http is reduced to its query part, anything else is treated as a query string.
{{ loginParseError }}
Telegram's own reference implementation rejects a callback older than 86400 seconds. The signature itself stays valid forever, so this limit is yours to enforce.
Paste an Update to decode it.
{{ decoded.typeDesc }}
{{ decoded.text || '(empty)' }}Offsets are UTF-16 code units, not characters. An emoji before the entity shifts every following offset by 2, which is the usual reason a slice comes out wrong in Python or Go.
{{ decoded.keyboard.note }}
{{ decoded.keyboard.warn }}
{{ decoded.tree }}Paste an initData string and enter the bot token.
{{ verifyResult.reason }}
A valid signature never expires on its own — auth_date is the only thing standing between you and an initData string replayed months later. Always enforce a max age server-side.
{{ verifyResult.steps.join('\n') }}Fill in at least a user ID and first name, then press Sign.
Telegram.WebApp.initData. {{ signResult.initData }}{{ signResult.dataCheckString }}Paste a Login Widget callback and enter the bot token.
{{ loginResult.reason }}
Reported separately from the signature on purpose: a correctly signed callback that is six months old is both valid and unusable, and merging the two verdicts hides the replay window.
{{ loginResult.steps.join('\n') }}Both schemes finish with HMAC-SHA256 over the same kind of data-check string — every field except hash, sorted alphabetically, joined with newlines. Only the secret key differs: SHA256(bot_token) here, HMAC-SHA256("WebAppData", bot_token) for initData. Using the wrong derivation is the usual reason a correct-looking implementation never matches a real callback.
{{ exportScheme }} verification in {{ activeLang.label }} — signature and age. {{ exportedCode }}The Webhook Tester answers "what did Telegram actually send me?". An Update carries exactly one payload field out of more than twenty, and which one it is decides how your handler should branch. It also decodes entities, where the classic bug lives: offsets are counted in UTF-16 code units, not characters. One emoji earlier in the message shifts every later offset by two, so naive slicing in Python or Go cuts text in the wrong place. Replay sends the same payload to your own endpoint so you can re-trigger a bug without waiting for a real user.
The initData tab covers Mini App authentication. Telegram signs the launch parameters with a key derived from your bot token, and your backend must recompute that signature before trusting the user id inside — otherwise anyone can open your Mini App URL with a hand-written user field and be whoever they like. The algorithm is deliberate about two things people get wrong: the secret key is HMAC-SHA256("WebAppData", bot_token) — the token is the message, not the key — and the fields must be sorted alphabetically before hashing.
Signing exists for testing. A signed string is only valid for the bot token that produced it, so it is useful for fixtures and local harnesses and useless as an attack: without the real token, no forged initData will ever verify against a real bot.
The Login Widget tab covers the other authentication scheme Telegram offers — the "Log in with Telegram" button on an ordinary website, which redirects (or calls data-onauth) with id, first_name, auth_date and a hash. It ends in the same HMAC-SHA256 over the same kind of sorted data-check string as initData; the one difference is the key. Here the secret key is SHA256(bot_token) — a plain hash of the token. For initData it is HMAC-SHA256("WebAppData", bot_token). Both schemes live on this page precisely so they share one HMAC implementation, one constant-time comparison and one set of known-answer vectors, including a guard asserting that each scheme's payload is rejected under the other's key derivation.
Companion tool: this page covers the inbound half — what arrives and whether to trust it. For the outbound half — MarkdownV2 and HTML escaping, message bubble preview, inline keyboards, the 64-byte callback_data limit, BotFather commands and deep links — use Telegram Studio.
Privacy: 100% client-side. The bot token is used with the Web Crypto API in your browser and is never transmitted. "Remember" keeps it in sessionStorage, which is cleared when the tab closes; webhook history is kept in localStorage on this device only.