DEV Community

Haskell Thurber
Haskell Thurber

Posted on

How to Accept Payments in a Telegram Mini App Using Stars (Step-by-Step Guide)

Telegram Stars are a built-in payment system for Mini Apps — no Stripe, no App Store fees, no payment gateway needed. Users pay with Stars they already have in Telegram, and you get paid directly.

In this tutorial, I'll show you exactly how to implement Stars payments in a Node.js + Express backend, based on what I built for WhisprMe — an anonymous messaging Mini App.

What Are Telegram Stars?

Stars are Telegram's in-app currency. Users buy them inside Telegram and spend them in Mini Apps and bots. As a developer, you:

  1. Create an invoice via the Bot API
  2. Handle the pre-checkout query
  3. Confirm the payment
  4. Deliver the digital good

There are zero platform fees for the first withdrawal (Telegram takes a cut on subsequent ones). The entire flow happens inside Telegram — no redirects, no card forms.

Prerequisites

  • A Telegram Bot (create one via @BotFather)
  • Node.js 18+
  • The telegraf package (or node-telegram-bot-api)
  • A Telegram Mini App connected to your bot

Step 1: Create the Invoice Endpoint

Your frontend calls this endpoint to get a payment link:

// routes.js
const { Telegraf } = require('telegraf');
const bot = new Telegraf(process.env.BOT_TOKEN);

app.post('/api/create-invoice', async (req, res) => {
  const { telegramId, itemType, amount } = req.body;

  try {
    const invoiceLink = await bot.telegram.createInvoiceLink({
      title: 'Premium Feature',
      description: 'Unlock premium features in WhisprMe',
      payload: JSON.stringify({ 
        userId: telegramId, 
        type: itemType 
      }),
      provider_token: '',  // Empty for Stars!
      currency: 'XTR',     // XTR = Telegram Stars
      prices: [{ 
        label: 'Premium', 
        amount: amount  // in Stars (1 Star = 1 unit)
      }]
    });

    res.json({ invoiceLink });
  } catch (err) {
    console.error('Invoice error:', err);
    res.status(500).json({ error: 'Failed to create invoice' });
  }
});
Enter fullscreen mode Exit fullscreen mode

Key points:

  • provider_token must be an empty string for Stars
  • currency must be 'XTR'
  • amount is in whole Stars (no decimals)
  • The payload is your custom data — you'll get it back after payment

Step 2: Handle Pre-Checkout Query

Before Telegram charges the user, it sends a pre-checkout query. You must answer within 10 seconds:

// bot.js
bot.on('pre_checkout_query', async (ctx) => {
  try {
    const payload = JSON.parse(ctx.preCheckoutQuery.invoice_payload);

    // Validate the purchase
    // - Does the user exist?
    // - Is the item still available?
    // - Is the price correct?

    await ctx.answerPreCheckoutQuery(true);
  } catch (err) {
    await ctx.answerPreCheckoutQuery(false, 
      'Something went wrong. Please try again.'
    );
  }
});
Enter fullscreen mode Exit fullscreen mode

Critical: If you don't respond to pre_checkout_query within 10 seconds, the payment fails silently. Make your validation fast!

Step 3: Handle Successful Payment

After the user pays, Telegram sends a successful_payment message:

bot.on('message', async (ctx) => {
  if (!ctx.message.successful_payment) return;

  const payment = ctx.message.successful_payment;
  const payload = JSON.parse(payment.invoice_payload);
  const stars = payment.total_amount;

  // Record the transaction
  await db.query(
    \`INSERT INTO transactions 
     (user_id, type, stars_amount, telegram_payment_id) 
     VALUES ($1, $2, $3, $4)\`,
    [payload.userId, payload.type, stars, 
     payment.telegram_payment_charge_id]
  );

  // Deliver the digital good
  if (payload.type === 'premium') {
    await db.query(
      'UPDATE users SET subscription_tier = $1 WHERE telegram_id = $2',
      ['premium', payload.userId]
    );
  }

  // Notify the user
  await ctx.reply(
    \`Payment successful! You paid ${stars} Stars.\`
  );
});
Enter fullscreen mode Exit fullscreen mode

Step 4: Open the Invoice from Your Mini App

In your React frontend, use the Telegram WebApp SDK:

// React component
const handlePurchase = async (itemType, amount) => {
  const response = await fetch('/api/create-invoice', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ 
      telegramId: window.Telegram.WebApp.initDataUnsafe.user.id,
      itemType,
      amount
    })
  });

  const { invoiceLink } = await response.json();

  // This opens Telegram's native payment UI
  window.Telegram.WebApp.openInvoice(invoiceLink, (status) => {
    if (status === 'paid') {
      // Refresh UI, show success
      showNotification('Payment successful!');
      window.Telegram.WebApp.HapticFeedback.notificationOccurred('success');
    }
  });
};
Enter fullscreen mode Exit fullscreen mode

The Complete Payment Flow

Here's what happens when a user clicks "Buy":

  1. Frontend calls /api/create-invoice
  2. Backend creates an invoice link via Bot API
  3. Frontend opens it with WebApp.openInvoice()
  4. Telegram shows native payment UI
  5. User confirms payment with Stars
  6. Telegram sends pre_checkout_query → your bot answers true
  7. Telegram sends successful_payment → you deliver the good
  8. Frontend callback fires with status === 'paid'

The entire flow takes about 2 seconds. No redirects, no card forms, no 3D Secure.

Real-World Tips from Building WhisprMe

After processing real payments in WhisprMe, here's what I learned:

1. Always validate in pre_checkout_query. Don't just auto-approve. Check that the item exists and the price hasn't changed.

2. Use the payload wisely. Encode everything you need to fulfill the order — user ID, item type, quantity. You'll thank yourself during debugging.

3. Idempotency matters. Store telegram_payment_charge_id and check for duplicates. Telegram can sometimes send duplicate successful_payment events.

4. Stars pricing psychology. We found that 15 Stars works better than 10 or 20 for premium features. It feels "affordable but not cheap."

5. Haptic feedback. That HapticFeedback.notificationOccurred('success') after payment makes a huge difference in user experience.

Setting Up in Production

Make sure your webhook is properly configured for payment events:

// index.js
const express = require('express');
const app = express();

// Important: set webhook with allowed_updates
await bot.telegram.setWebhook(
  'https://yourdomain.com/bot-webhook',
  { allowed_updates: ['message', 'pre_checkout_query'] }
);

app.use('/bot-webhook', 
  express.json(), 
  (req, res) => bot.handleUpdate(req.body, res)
);
Enter fullscreen mode Exit fullscreen mode

Conclusion

Telegram Stars make payments ridiculously simple compared to traditional payment gateways. No KYC for users, no payment forms, instant delivery — it's the kind of payment UX we've always wanted.

If you're building a Telegram Mini App, Stars should be your default monetization strategy. The conversion rates are significantly higher than traditional payment methods because users don't leave the app.

Try it yourself: @WhisprMe_bot — send anonymous messages, unlock premium features with Stars.


What payment challenges have you faced with Telegram Mini Apps? Drop a comment below!

Top comments (0)