Compose a createInvoiceLink payload for Telegram Stars or a traditional card provider, with every limit checked as you type. The trap this page exists for is amount units: 1 means one star, 100 means one dollar, and 250 means two hundred and fifty yen. The live total is always rendered in the units the invoice will actually charge.
Your own opaque identifier — Telegram never shows it and hands it straight back on pre_checkout_query and successful_payment. The limit is 128 bytes, not 128 characters, so Cyrillic or emoji cost two to four bytes each.
{{ unitNote }}
is_flexible means the final price depends on the shipping address, so your bot must answer shipping_query. It is independent of max_tip_amount — tips work with or without it.
Tipping, name/phone/email collection, shipping and is_flexible are not supported for payments in Telegram Stars, so those fields are omitted here rather than shown greyed out. Stars invoices carry no billing details at all — Telegram handles the whole checkout.
Paste the JSON parameters you send to createInvoiceLink, or a whole cURL / Node.js / Python / PHP snippet containing them — the first complete JSON object in the text is used.
What cannot be decoded here: the https://t.me/$… link that createInvoiceLink returns is an opaque server-side handle with no client-readable contents, and a bare invoice_payload string is whatever bytes your own bot chose. Neither carries the invoice, so this tab does not pretend to parse them.
Using the invoice from the {{ buildMode === 'stars' ? 'Stars (XTR)' : 'Card payment' }} tab: {{ form.title || 'Untitled invoice' }}, total {{ formatMoney(totalAmount, payloadCurrency) }}.
Fix the {{ blockingIssues.length }} blocking issue(s) on that tab first — a real bot would never receive checkout events for an invoice Telegram rejects.
Telegram sends this Update to your webhook. You must call answerPreCheckoutQuery within 10 seconds or Telegram cancels the payment automatically.
{{ JSON.stringify(flowPreCheckoutUpdate, null, 2) }}Telegram received no answer within 10 seconds and cancelled the payment — the buyer sees a failure and is never charged.
answerPreCheckoutQuery was called with ok: false — Telegram shows "{{ flowPreCheckoutAnswered.error_message }}" to the buyer and the payment stops here. No successful_payment update is ever sent for this attempt.
Telegram completed the charge and sends this Update to your webhook — the same at-least-once delivery guarantee as any other update, so it can arrive more than once.
{{ JSON.stringify(flowSuccessfulPaymentUpdate, null, 2) }}{{ flowDeliveryResult.firstTime ? 'Granted' : 'Already processed' }} delivery #{{ flowDeliveryResult.deliveryCount }} for this charge id — {{ flowDeliveryResult.firstTime ? 'purchase granted' : 'no second grant, exactly as an idempotent handler should behave' }}.
refundStarPayment only exists for payments in Telegram Stars. A card payment's refund goes through that provider's own dashboard or API, entirely outside the Bot API — there is nothing to simulate here for this invoice's currency ({{ payloadCurrency }}).
Refunding a Stars payment calls refundStarPayment with the buyer's user_id and the telegram_payment_charge_id from step 3.
{{ JSON.stringify({ user_id: SIM_USER_ID, telegram_payment_charge_id: flowChargeId }, null, 2) }}{{ flowRefundResult.ok ? 'Refunded' : 'Rejected' }} {{ flowRefundResult.reason }}
{{ form.title || 'Untitled invoice' }}
{{ form.description || 'No description yet.' }}
That photo_url did not load in this browser. Telegram fetches it server-side, so a URL behind auth or hotlink protection fails there too.
Layout is indicative. Telegram renders the real invoice; what matters here is that the numbers read the way they will charge.
Title, description, payload bytes, prices and provider rules all check out for this mode.
{{ payloadJson }}No successful_payment has been delivered yet in this simulation.
| telegram_payment_charge_id | granted | deliveries | refunded |
|---|---|---|---|
| {{ chargeId }} | {{ entry.granted ? '✓' : '—' }} | {{ entry.deliveryCount }} | {{ entry.refunded ? '✓' : '—' }} |
This ledger is exactly what the "idempotency" lesson is about: deliveries can exceed 1 (Telegram's at-least-once guarantee), but granted only ever becomes true once per charge id. A real bot needs its own durable version of this table — this one lives only in page memory and disappears on reload.
Paste createInvoiceLink parameters on the left.
{{ decoded.reason }}
| Field | Value |
|---|---|
| {{ row.key }} | {{ row.value }} |
{{ decodedTotalLabel }}
Every rule this tool knows about passes for the pasted parameters.
createInvoiceLink in {{ activeLang.label }}. The response's result is the link you send to the buyer. {{ exportedCode }}createInvoiceLink has roughly twenty parameters and two mutually exclusive modes, and almost every hour lost to it is lost to the same thing: amount units. Amounts are always integers in the currency's smallest unit. For traditional currencies that means amount: 100 is $1.00, not one hundred dollars. For zero-decimal currencies such as JPY the smallest unit is the currency, so amount: 250 is ¥250 — writing 25000 because "it worked for USD" charges a hundred times too much. For Telegram Stars there is no minor unit either: amount: 250 is 250 ⭐. This page keeps a per-currency exponent table and renders the running total in real units so the mistake is visible before it reaches a buyer.
The second recurring trap is payload. It is your own opaque string, returned to you untouched on pre_checkout_query and successful_payment, and its limit is 1–128 bytes. A .length check passes on a Cyrillic or emoji payload that Telegram then rejects, because UTF-8 spends two to four bytes per such character. The counter here measures encoded bytes.
Stars versus card is not a cosmetic switch. Stars invoices use currency: "XTR", carry no provider_token, must contain exactly one price line, and support none of the billing-detail flags — no tipping, no name, phone, email or shipping address, no is_flexible. Card invoices need a provider_token from @BotFather and may use all of them. Those are separate tabs here so the unsupported fields are absent rather than greyed out, and the Decode tab cross-checks a pasted payload for the mismatch in both directions.
Whatever createInvoiceLink returns is an opaque https://t.me/$… handle. There is nothing inside it to decode in a browser, so the Decode tab works on the parameters you send, not on the link you get back. This page deliberately stops at building that payload — it does not call the real createInvoiceLink API, which would need a bot token sent over the network for the first time on this page and a real live-testing decision, not a default.
The Payment Flow tab is a local simulator of what happens after the invoice is sent: Telegram answers with a pre_checkout_query your bot must confirm via answerPreCheckoutQuery within 10 real seconds (a countdown here actually expires if you don't answer, exactly like production), then — only if you answered ok: true in time — a successful_payment update your bot must grant access for. That update can arrive more than once: Telegram's Bot API delivers every update at least once, never exactly once, so a "Simulate duplicate delivery" button re-sends the identical update and a small ledger shows the purchase getting granted on delivery #1 and correctly not re-granted on delivery #2. refundStarPayment — Stars only, since a card payment's refund goes through that provider's own dashboard, not the Bot API — gets the same treatment: refunding twice is rejected, not silently repeated. None of this contacts Telegram; paste any step's JSON into Telegram Webhook Tester's Replay tab to send it to your own real endpoint.
Companion tools: compose the message that carries the invoice button with Telegram Studio, and verify the pre_checkout_query and successful_payment updates that come back with Telegram Webhook Tester.
Privacy: 100% client-side. Nothing is submitted anywhere and no provider token is transmitted — it is only interpolated into the snippet shown on screen. The Payment Flow tab is a pure local simulation and contacts nobody, Telegram included. The single exception on this whole page is the optional photo_url: previewing it loads that image directly from the address you typed, exactly as your browser would for any image.