DEV Community

Marylyn Roundy
Marylyn Roundy

Posted on Fully Autonomous

How to accept Telegram Stars in your bot (the full flow, plus a no-code shortcut)

If your Telegram bot sells anything digital (an ebook, a course, a preset pack, access to a private channel), Telegram expects you to charge in Telegram Stars. Card providers are only allowed for physical goods. This post walks through the whole Stars payment flow with a small working example, the gotchas that cost people money, and how much you actually receive.

Why Stars and not cards?

Telegram's payments page for digital goods is clear: digital goods and services sold inside Telegram must be paid in Stars (currency code XTR). This follows the App Store and Play Store rules. Buyers get Stars through normal in-app purchases, so paying takes two taps and they never type a card number.

The flow in six steps

  1. Send an invoice with sendInvoice (or make a reusable link with createInvoiceLink), using currency: "XTR".
  2. When the buyer taps Pay, you receive a pre_checkout_query update.
  3. Approve or reject it with answerPreCheckoutQuery within 10 seconds, or the payment is cancelled.
  4. You then receive a message containing successful_payment.
  5. Save successful_payment.telegram_payment_charge_id, because you need it for refunds.
  6. Deliver the product.

For digital goods you don't need a provider_token; leave it empty.

A minimal working bot (Node 18+, no libraries)

Save this as bot.mjs and run it with BOT_TOKEN=... node bot.mjs. It uses long polling, so you don't need a server or a webhook.

const TOKEN = process.env.BOT_TOKEN;
const api = (method, body) =>
  fetch(`https://api.telegram.org/bot${TOKEN}/${method}`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(body),
  }).then((r) => r.json());

const delivered = new Set(); // use a real database in production

async function handle(update) {
  const msg = update.message;

  if (msg?.text === '/buy') {
    await api('sendInvoice', {
      chat_id: msg.chat.id,
      title: 'Preset pack',
      description: '12 photo presets, delivered instantly',
      payload: `order:${msg.from.id}:${Date.now()}`,
      currency: 'XTR',
      prices: [{ label: 'Preset pack', amount: 250 }], // 250 Stars
    });
  }

  if (update.pre_checkout_query) {
    // Check stock / that the order is still valid. You have 10 seconds.
    await api('answerPreCheckoutQuery', {
      pre_checkout_query_id: update.pre_checkout_query.id,
      ok: true,
    });
  }

  const paid = msg?.successful_payment;
  if (paid && !delivered.has(paid.telegram_payment_charge_id)) {
    delivered.add(paid.telegram_payment_charge_id);
    await api('sendMessage', {
      chat_id: msg.chat.id,
      text: 'Thank you! Here is your download link: …',
    });
  }
}

let offset = 0;
for (;;) {
  const { result = [] } = await api('getUpdates', { offset, timeout: 30 });
  for (const u of result) {
    offset = u.update_id + 1;
    await handle(u).catch(console.error);
  }
}
Enter fullscreen mode Exit fullscreen mode

To refund a buyer, call:

await api('refundStarPayment', { user_id, telegram_payment_charge_id });
Enter fullscreen mode Exit fullscreen mode

Gotchas that cost real money

  • Deliver only on successful_payment. Answering the pre-checkout query does not mean the buyer paid.
  • Deduplicate by telegram_payment_charge_id. Network retries happen, and you don't want to deliver twice or record a sale twice.
  • Keep the pre-checkout handler fast. Ten seconds includes your database calls. Never put slow work there.
  • Support and terms. Telegram's checklist asks bots that take payments to have a clear way to reach support (it mentions /paysupport) and easy access to your terms. You handle disputes and refunds yourself.
  • Subscriptions. createInvoiceLink accepts subscription_period, which currently must be 2592000 (30 days). That's how you charge monthly for a private channel.
  • Turn on two-step verification on the Telegram account that owns the bot. Telegram recommends it, and it's needed when you withdraw Stars.

How much do you actually receive?

Buyers pay Apple or Google prices for Stars, and the app stores take their cut. When you withdraw through Fragment, Telegram's rate is about $0.013 per Star, paid in TON, and new Stars can only be withdrawn after a holding period.

In practice:

  • At 50 Stars per $1 (a common default), you receive about $0.65 per dollar of list price.
  • To receive about $1 per dollar, price at roughly 77 Stars per $1.

The no-code shortcut

Disclosure: I'm part of the team building TeleCrow.

If you'd rather not run and maintain the code above, TeleCrow is a no-code dashboard for this kind of bot. You connect your own bot's token from BotFather and turn on Stars in a few taps. The Stars go to your own bot's balance, and you withdraw them on Fragment yourself. TeleCrow handles:

  • shops with automatic file delivery;
  • paid channels and groups with expiry, renewal reminders, trials and promo codes.

There's a free plan, and TeleCrow takes 0% commission on sales.

Either way, the flow above is what happens under the hood. If you build it yourself, get the six steps and the gotchas right and Stars become a very smooth checkout.

Top comments (1)

Collapse
 
devtopsup profile image
DEV •

Dev-verify

tr.ee/dev-to