NxCreateDocs

Payments

Sell digital goods with Telegram Stars, or accept real-world payment providers — invoices, pre-checkout, and refunds.

Telegram bots can charge users two ways: Telegram Stars — Telegram's own in-app currency, no payment provider or bank account needed — or a traditional payment provider (Stripe and others) for real-world currency. Both flows go through the same three Bot API steps: send an invoice, answer the pre-checkout query, then handle the successful payment.

Telegram Stars

Stars are Telegram's built-in currency for digital goods — bot features, premium content, in-app items. There is no provider token, no KYC, and no bank account required: users buy Stars once inside Telegram, then spend them across any bot.

Send a Stars invoice with ctx.replyWithInvoice (or bot.telegram.sendInvoice for a specific chat). The two things that mark it as a Stars invoice: an empty provider_token and currency: 'XTR'.

bot.command('buy', async (ctx) => {
  await ctx.replyWithInvoice({
    title: 'Premium pack',
    description: 'Unlock the pro toolkit for this bot.',
    payload: 'premium_pack_' + ctx.from.id,
    provider_token: '',      // empty for Telegram Stars
    currency: 'XTR',         // Stars currency code
    prices: [{ label: 'Premium pack', amount: 100 }], // 100 Stars — no decimal subdivision
  });
});
For Stars, amount is the number of Stars directly — unlike real currencies, there's no ×100 minor-unit conversion. A price of 100 charges exactly 100 Stars.

You can also attach a photo_url to the invoice for a product image, and split prices into multiple line items if you're charging for a bundle — Telegram sums them for the displayed total.

Pre-checkout

Before Telegram charges the user, it sends your bot a pre_checkout_query — your last chance to confirm the order is still valid (stock available, price unchanged) before money moves. You must answer within 10 seconds or the payment is cancelled automatically.

bot.on('pre_checkout_query', async (ctx) => {
  const stillValid = await checkStockOrPrice(ctx.preCheckoutQuery.invoice_payload);

  if (stillValid) {
    await ctx.answerPreCheckoutQuery(true);
  } else {
    await ctx.answerPreCheckoutQuery(false, 'This item is no longer available.');
  }
});
If you don't answer the pre-checkout query at all, Telegram treats it as a failure and refunds the user automatically after the timeout — always answer explicitly, even just ctx.answerPreCheckoutQuery(true), for every invoice you send.

Successful payment

Once the charge clears, Telegram delivers a message containing a successful_payment object. This is where you actually grant whatever was purchased — Telegraf recognises it as a message sub-type, so you can listen for it directly.

bot.on('successful_payment', async (ctx) => {
  const payment = ctx.message.successful_payment;

  await db.setProp(ctx.from.id, 'isPremium', true);
  await db.setProp(ctx.from.id, 'lastChargeId', payment.telegram_payment_charge_id);

  await ctx.reply('Payment received — premium unlocked! ⭐');
});

Save telegram_payment_charge_id whenever you handle a successful payment — it's the only identifier Telegram accepts later for issuing a refund.

Refunds

Stars purchases can be refunded from your bot with refundStarPayment, using the user's ID and the charge ID you saved from the successful payment. There's no dashboard step — it's a direct API call.

bot.command('refund', async (ctx) => {
  const chargeId = await db.getProp(ctx.from.id, 'lastChargeId');
  if (!chargeId) return ctx.reply('No Stars payment found to refund.');

  await bot.telegram.refundStarPayment(ctx.from.id, chargeId);
  await db.setProp(ctx.from.id, 'isPremium', false);
  await ctx.reply('Refunded — your Stars have been returned.');
});
Refunds through a traditional provider (Stripe etc.) go through that provider's own dashboard or API instead — refundStarPayment only applies to Stars (XTR) payments.

Provider payments

For real-world currency instead of Stars, get a provider_token from @BotFather (Payments → connect a provider like Stripe) and pass it into the same invoice call, along with a real ISO currency code.

bot.command('subscribe', async (ctx) => {
  await ctx.replyWithInvoice({
    title: 'Monthly subscription',
    description: 'Full access, billed monthly.',
    payload: 'sub_' + ctx.from.id,
    provider_token: process.env.STRIPE_PROVIDER_TOKEN,
    currency: 'USD',
    prices: [{ label: 'Subscription', amount: 999 }], // $9.99 — minor units, so ×100
  });
});

The pre_checkout_query and successful_payment handlers above work identically for provider payments — the only differences are the invoice fields and that amounts are in minor currency units (cents), not whole Stars.

Telegram StarsProvider (Stripe, etc.)
provider_tokenEmpty stringFrom @BotFather
currencyXTRISO code, e.g. `USD`
amountWhole StarsMinor units (cents)
RefundsrefundStarPaymentProvider's own dashboard/API
SetupNone — works immediatelyRequires provider approval
Last updated August 11, 2026
Was this page helpful?