Telegram Webhook Tester

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.

HMAC-SHA256 · local only
Bot token

Update payload (JSON)

{{ updateError }}

Paste the raw body your webhook endpoint received, or an update from getUpdates.

Replay to your endpoint

Target URL
Secret token
Extra headers (JSON object)
{{ replayResult.label }} {{ replayResult.detail }} Sends a POST with the payload above.

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.

History ({{ history.length }})

No updates yet. Replay one above and it will be kept here on this device.

{{ h.type }} {{ h.preview }}

initData string

The raw value of window.Telegram.WebApp.initData, exactly as received — do not re-encode it.

Freshness limit

Max age seconds

Signing form

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.

User ID *
First name *
Last name
Username
Language
Photo URL
auth_date
query_id
start_param

{{ signIssue }}

Signs with the bot token from the toolbar.

Login Widget callback

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 }}

Freshness limit

Max age seconds

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.

Decoded
{{ decoded.type }} No payload

Paste an Update to decode it.

Verification
{{ verifyResult.valid ? 'Signature valid' : 'Signature invalid' }} Awaiting input

Paste an initData string and enter the bot token.

Signed initData
Signed Awaiting input

Fill in at least a user ID and first name, then press Sign.

Login Widget verification
{{ loginResult.valid ? 'Signature valid' : 'Signature invalid' }} Awaiting input

Paste a Login Widget callback and enter the bot token.

Login Widget vs Mini App initData

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.

Server-side {{ exportScheme }} verification in {{ activeLang.label }} — signature and age.
{{ exportedCode }}
About Telegram Webhook Tester

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.

{{ toast.msg }}