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 · replay sends live
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)
Timeout ms — browsers enforce no timeout of their own, so a dead endpoint would otherwise hang "Sending…" indefinitely
{{ 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. Multiple scripted sends (including a duplicate-delivery scenario) live on the Test Suite tab.

History ({{ history.length }})

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

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

Fixtures

Runs each fixture against the Target URL configured on the Webhook Tester tab. A fixture with repeat > 1 IS the duplicate-delivery scenario — Telegram guarantees at-least-once delivery, so your handler must tolerate the same update_id arriving more than once.

Set a Target URL on the Webhook Tester tab before running fixtures.

{{ decodeUpdate(f.update).type }} · update_id {{ f.update.update_id }}
Repeat Delay ms
{{ f.result.verdict }} {{ f.result.attempts.map(a => a.timedOut ? 'timeout' : a.blocked ? 'blocked' : ('HTTP ' + a.status)).join(', ') }} {{ f.result.error }}

No fixtures yet. Add the payload currently on the Webhook Tester tab, or load the starter set above.

What a verdict actually means: pass means every send got a successful, byte-identical HTTP response — a reachability and consistency check. unknown means the response was blocked from JS by CORS, not that the request failed. This tool cannot see your database, so a pass on a duplicate-delivery fixture is evidence your endpoint behaved consistently, not proof your handler deduplicated the update server-side.

Opt-in and live. This is the only feature on this page that sends the bot token over the network — directly to api.telegram.org (verified to support CORS from any origin), never anywhere else. getUpdates does not work while a webhook is set — check first, below. This tool never deletes a webhook and never auto-advances offset unless you tick the box for it, since a higher offset permanently confirms (drops) those updates from Telegram's queue and could steal them from a production poller.

1. Check webhook status

{{ webhookInfoError }} {{ webhookInfo.url ? 'Webhook set' : 'No webhook' }} — getUpdates will not work until this webhook is removed (not something this tool will do for you). — getUpdates is usable.

2. Poll for updates

offset
limit timeout (s) long polling — this tab will wait up to {{ getUpdatesTimeoutSec }}s for a response
allowed_updates
GET https://api.telegram.org/bot•••/getUpdates

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.

Suite results
{{ fixtures.length }} fixture{{ fixtures.length === 1 ? '' : 's' }}

No fixtures yet.

Live updates
{{ getUpdatesResult.updates.length }} update{{ getUpdatesResult.updates.length === 1 ? '' : 's' }} Error No poll yet

Enter the bot token in the toolbar and press "Poll now".

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, bounded by a configurable timeout — browsers enforce none of their own, so a dead endpoint used to leave "Sending…" showing indefinitely.

The Test Suite tab turns one-off replays into a saved suite: name a fixture, set how many times to send it, and run the whole set against the same target. A fixture sent more than once is the duplicate-delivery scenario — Telegram's own at-least-once delivery guarantee means your handler must tolerate the same update_id arriving twice. A verdict here is a reachability and response-consistency check, not proof of server-side deduplication: this tool cannot see your database, only whether your endpoint answered the same way every time.

The Live getUpdates tab is opt-in and genuinely live — the only feature on this page whose request carries the bot token over the network, sent directly to api.telegram.org (confirmed to allow cross-origin browser requests). It cannot run alongside an active webhook, so a webhook-status check comes first. Polling defaults to non-destructive: offset is left blank unless you type one, and auto-advancing it — which permanently confirms those updates out of Telegram's queue — is an explicit opt-in checkbox, off by default, because a production poller might still be depending on receiving them.

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: Client-side, with one disclosed exception. The bot token is used with the Web Crypto API in your browser and is never transmitted — except on the Live getUpdates tab, where it is necessarily sent to Telegram's own api.telegram.org to make that specific API call, and nowhere else. "Remember" keeps the token in sessionStorage, cleared when the tab closes; webhook history and saved fixtures are kept in localStorage on this device only. Replay and Test Suite sends go straight from your browser to whatever Target URL you enter — also disclosed, since that URL is yours to choose, not this site's.

{{ toast.msg }}