Skip to content
✦ AddThisBot
Guide · checked against Telegram docs on 2026-10-28

Accept Telegram Stars in your bot.

The full flow for digital goods: an XTR invoice, the 10-second pre-checkout answer, delivery, refunds and 30-day subscriptions. With TypeScript for a Cloudflare Worker.

currency code
XTR
provider_token
""
item in prices
1
to answer pre-checkout
10 s
subscription_period (30 days)
2592000
max Stars per subscription
10,000

How a Stars payment works

Telegram Stars are the in-app currency for digital goods in bots and Mini Apps. Users buy Stars in Telegram (through Apple or Google in-app purchases, or @PremiumBot). Your bot sells to them with currency code XTR. There is no card form and no payment provider to set up.

One purchase is eight messages between three parties. Step through them:

User

Telegram app

Telegram

Bot API

Your bot

Cloudflare Worker

Step 1 / 8

The user asks to buy

A command, a button or a Mini App action. Your bot decides what to sell and for how many Stars.

{ "message": { "text": "/pro", "chat": { "id": 123 } } }

The rules in one table

These come from the Bot API reference for sendInvoice, createInvoiceLink and the Stars payments page.

  • currency "XTR" All digital goods are sold in Telegram Stars.
  • provider_token "" (empty string) Only physical goods use a payment provider.
  • prices Array with exactly one LabeledPrice Stars invoices take one price item. amount is a whole number of Stars.
  • title / description 1-32 / 1-255 characters Shown on the invoice.
  • payload 1-128 bytes Your own order ID. Not shown to the user. Comes back in every update.
  • max_tip_amount Not supported No tips with Stars.
  • answerPreCheckoutQuery Within 10 seconds Otherwise the transaction is canceled.
  • subscription_period 2592000 (createInvoiceLink only) 30 days. Price at most 10,000 Stars.
  • refundStarPayment user_id + telegram_payment_charge_id Store the charge ID from successful_payment.

Set up the bot and Worker

You need three things: a bot token from @BotFather, a public HTTPS endpoint, and a place to store purchases. A Cloudflare Worker gives you the endpoint. D1 (SQLite) stores purchases. KV or Durable Objects also work.

1. A tiny Bot API client

No SDK. One function that posts JSON and throws when Telegram says ok: false.

src/tg.ts
// tg.ts — tiny Bot API client for Cloudflare Workers (no SDK needed)
export interface Env {
  BOT_TOKEN: string;      // from @BotFather. Store with: wrangler secret put BOT_TOKEN
  WEBHOOK_SECRET: string; // any random string; also passed to setWebhook
  DB: D1Database;         // purchases table (or KV / Durable Objects)
}

export async function tg<T = unknown>(env: Env, method: string, body: object): Promise<T> {
  const res = await fetch(`https://api.telegram.org/bot${env.BOT_TOKEN}/${method}`, {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify(body),
  });
  const data = (await res.json()) as { ok: boolean; result: T; description?: string };
  if (!data.ok) throw new Error(`${method} failed: ${data.description}`);
  return data.result;
}

2. A purchases table

The charge ID is the primary key. If Telegram retries an update, the second insert does nothing.

schema.sql
-- schema.sql (D1). Keep every charge ID: refunds need it.
CREATE TABLE IF NOT EXISTS purchases (
  charge_id TEXT PRIMARY KEY,   -- telegram_payment_charge_id
  user_id   INTEGER NOT NULL,
  payload   TEXT NOT NULL,      -- e.g. "pro:-100123456"
  stars     INTEGER NOT NULL,
  at        INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS purchases_user ON purchases (user_id);
CREATE INDEX IF NOT EXISTS purchases_payload ON purchases (payload);

3. Point Telegram at the Worker

Pass a secret_token. Telegram sends it back in the X-Telegram-Bot-Api-Secret-Token header, so your Worker can drop requests that do not come from Telegram. Ask for pre_checkout_query updates in allowed_updates. successful_payment arrives inside a normal message.

terminal
# Point Telegram at your Worker. Telegram sends the secret back in a header.
curl "https://api.telegram.org/bot$BOT_TOKEN/setWebhook" \
  -d "url=https://my-bot.example.workers.dev/" \
  -d "secret_token=$WEBHOOK_SECRET" \
  -d 'allowed_updates=["message","pre_checkout_query"]'

Send an invoice

There are two ways to ask for Stars. Both take the same fields.

sendInvoice

Sends an invoice message with a Pay button into a chat: a private chat, a group or a channel. Returns the sent Message.

createInvoiceLink

Returns a link to an invoice. Open it in a Mini App with openInvoice, or put it in a button. The only way to sell a subscription.

sendInvoice
await tg("sendInvoice", {
  chat_id: chatId,
  title: "Pro",
  description: "Unlock Pro for this group. One payment.",
  payload: `pro:${chatId}`,     // your own ID, 1-128 bytes
  provider_token: "",            // empty string for Stars
  currency: "XTR",               // Telegram Stars
  prices: [{ label: "Pro", amount: 150 }], // exactly one item
});
createInvoiceLink
// A link you can open from a Mini App, a button or a web page.
const url = await tg<string>(env, "createInvoiceLink", {
  title: "Pro",
  description: "Unlock Pro. One payment.",
  payload: `pro:${userId}`,
  provider_token: "",
  currency: "XTR",
  prices: [{ label: "Pro", amount: 150 }],
});
// url is an invoice link. Put it in a button, or open it in a Mini App.

Forwarded invoices: start_parameter

If start_parameter is empty, forwarded copies of the invoice keep a Pay button, so many users can pay from one invoice. If you set it, forwarded copies show a URL button that deep-links to your bot with that value as the start parameter. Either way, your bot decides in pre-checkout whether to accept the payment.

Answer pre_checkout_query in 10 seconds

When the user taps Pay, Telegram sends your bot a pre_checkout_query with the currency, the total_amount and your invoice_payload. Call answerPreCheckoutQuery with ok: true to go on. With ok: false, add an error_message; Telegram shows it to the user.

Deliver on successful_payment

If the payment goes through, your bot gets a message with a successful_payment object. It holds currency, total_amount, invoice_payload and telegram_payment_charge_id.

  • Save the charge ID first. You need it for refundStarPayment and for subscription changes.
  • Then deliver. Unlock the feature, send the file, add the credits.
  • Then confirm in chat. A short thank-you with what changed.

Full Worker code

The complete webhook handler. It checks the secret header, answers pre-checkout, stores the payment, and serves /pro, /terms, /support and /paysupport. Deploy with wrangler deploy after wrangler secret put BOT_TOKEN.

src/index.ts
// index.ts — a Cloudflare Worker bot that sells Pro for Telegram Stars
import { tg, type Env } from "./tg";

const PRO_STARS = 150;

export default {
  async fetch(req: Request, env: Env): Promise<Response> {
    // Only Telegram knows the secret we gave to setWebhook.
    if (req.headers.get("X-Telegram-Bot-Api-Secret-Token") !== env.WEBHOOK_SECRET) {
      return new Response("forbidden", { status: 403 });
    }
    const update: any = await req.json();

    // 1) pre_checkout_query: answer within 10 seconds or Telegram cancels.
    if (update.pre_checkout_query) {
      const q = update.pre_checkout_query;
      const valid =
        q.currency === "XTR" &&
        q.total_amount === PRO_STARS &&
        String(q.invoice_payload).startsWith("pro:");
      await tg(env, "answerPreCheckoutQuery", valid
        ? { pre_checkout_query_id: q.id, ok: true }
        : { pre_checkout_query_id: q.id, ok: false,
            error_message: "This invoice is out of date. Send /pro to get a new one." });
      return new Response("ok");
    }

    const msg = update.message;
    if (!msg) return new Response("ok");

    // 2) successful_payment: the only safe moment to deliver.
    if (msg.successful_payment) {
      const p = msg.successful_payment;
      await env.DB.prepare(
        "INSERT OR IGNORE INTO purchases (charge_id, user_id, payload, stars, at) VALUES (?, ?, ?, ?, ?)"
      ).bind(p.telegram_payment_charge_id, msg.from.id, p.invoice_payload, p.total_amount, Date.now()).run();
      await tg(env, "sendMessage", { chat_id: msg.chat.id, text: "⭐ Pro is on. Thank you!" });
      return new Response("ok");
    }

    // 3) Commands.
    const cmd = String(msg.text ?? "").split(/[\s@]/)[0];
    if (cmd === "/pro") {
      await tg(env, "sendInvoice", {
        chat_id: msg.chat.id,
        title: "Pro",
        description: "Unlock Pro for this chat. One payment, no renewal.",
        payload: `pro:${msg.chat.id}`,
        provider_token: "",
        currency: "XTR",
        prices: [{ label: "Pro", amount: PRO_STARS }],
      });
    } else if (cmd === "/terms") {
      await tg(env, "sendMessage", { chat_id: msg.chat.id, text: "Terms: https://example.com/terms" });
    } else if (cmd === "/paysupport" || cmd === "/support") {
      await tg(env, "sendMessage", { chat_id: msg.chat.id,
        text: "Payment problem? Reply here with what happened. We answer within 24 hours. Telegram support cannot help with purchases in this bot." });
    }
    return new Response("ok");
  },
};

TypeScript for Cloudflare Workers. Field names checked against the Bot API reference on 2026-10-28.

Monthly subscriptions in Stars

For recurring access, create the invoice with createInvoiceLink and subscription_period: 2592000. Today that value must be exactly 30 days, and the price can be at most 10,000 Stars. A user can hold more than one subscription to the same bot.

subscription.ts
// Monthly subscription: only createInvoiceLink accepts subscription_period.
const url = await tg<string>(env, "createInvoiceLink", {
  title: "Pro monthly",
  description: "Pro features. Renews every 30 days.",
  payload: `sub:${userId}`,
  currency: "XTR",
  prices: [{ label: "Pro monthly", amount: 100 }], // max 10,000 Stars
  subscription_period: 2592000,                    // must be exactly 30 days
});

// Each renewal arrives as a new successful_payment:
//   p.is_recurring           true for subscription payments
//   p.is_first_recurring     true for the first one
//   p.subscription_expiration_date   Unix time when this period ends

Use subscription_expiration_date from each payment as the end of access. To stop renewal from your side, call editUserStarSubscription:

cancel.ts
// Stop renewal. The user keeps access until the current period ends.
await tg(env, "editUserStarSubscription", {
  user_id: userId,
  telegram_payment_charge_id: chargeId,
  is_canceled: true,   // pass false to let the user re-enable it
});

Refunds and disputes

You handle disputes for what your bot sells. Telegram gives you refundStarPayment to return the Stars yourself.

refund.ts
// Full refund of one Stars payment. The Stars go back to the user.
await tg(env, "refundStarPayment", {
  user_id: userId,
  telegram_payment_charge_id: chargeId, // saved from successful_payment
});
// Then turn Pro off for that payload in your database.

Your bot must respond to /paysupport and handle payment issues on time. Tell users that Telegram support cannot help with purchases made in your bot. For the full terms, read section 6.2 of the Telegram Bot Platform Developer Terms of Service.

To audit income, getStarTransactions lists the bot's Star transactions (up to 100 per call) and getMyStarBalance returns the current balance.

Go-live checklist

From Telegram's live checklist for merchant bots, in short:

  • Turn on 2-step verification — for the Telegram account that owns the bot.
  • Add /terms — or another easy way to read your terms. Users must agree before they buy.
  • Add /support and /paysupport — and answer payment requests on time.
  • Say who supports purchases — Telegram support cannot help with purchases in your bot. Tell your users.
  • Keep payment records safe — stable hosting and backups, so you never lose who paid for what.
  • Accept the terms — Telegram Terms of Service and the Bot Platform Developer Terms.

Test without real Stars

Telegram has a separate test environment. You need a test account and a new bot made with @BotFather there. Payments in Stars can be tested freely in it.

terminal
# The test environment is a separate Telegram. Make a new bot there with @BotFather,
# then call the API with /test/ before the method name:
curl "https://api.telegram.org/bot$TEST_TOKEN/test/getMe"

To log in on iOS, tap the Settings icon 10 times, then Accounts → Login to another account → Test. On Telegram Desktop, open Settings, then Shift + Alt + right-click "Add Account" and pick "Test Server".

Common mistakes

✕ Delivering on pre_checkout_query

A yes to pre-checkout is not a payment. Deliver only after successful_payment arrives.

✕ Slow pre-checkout handler

Database calls, cold caches or retries can pass the 10-second limit. Keep the check small and fast.

✕ Two prices in the array

Stars invoices must have exactly one item in prices. Put discounts into the single amount.

✕ Losing the charge ID

Without telegram_payment_charge_id you cannot refund. Save it before you send the thank-you message.

✕ Paying twice on retries

Telegram can resend an update if your webhook fails. Make the insert idempotent (charge_id as primary key).

✕ No /paysupport

Telegram asks paid bots to handle payment issues there. Add it before launch.

✕ Forwarded invoices you did not plan for

With an empty start_parameter, forwarded copies keep a Pay button. Check the payload and who pays.

How we price our bots

Our pattern: free to use, then a one-time Pro for 75 Stars or 150 Stars. Pro is scoped per user, per group or per chat, so one payment covers a whole group where it makes sense. Try one and watch the real invoice.

WhisperLock

Pro per user · about $3.00

⭐ 150
Nudge

Pro per user · about $3.00

⭐ 150
Anon Inbox

Pro per user · about $3.00

⭐ 150
SplitTabs

Pro per group · about $3.00

⭐ 150
HabitStreak

Pro per user · about $3.00

⭐ 150
Event RSVP

Pro per group · about $3.00

⭐ 150
AnonSay

Pro per group · about $3.00

⭐ 150
Party Pack

Pro per user · about $3.00

⭐ 150

USD is approximate. Stars cost more or less depending on where and how users buy them. See all Pro bots →

FAQ

Can I use Stripe or crypto for digital goods in a Telegram bot?

No. Telegram requires Telegram Stars (currency XTR) for digital goods and services sold in bots and Mini Apps. Other providers are only for physical goods and services.

What do I put in provider_token for Stars?

An empty string. The provider_token is only needed for physical goods. For Stars, the Bot API docs say to pass an empty string.

How long do I have to answer pre_checkout_query?

10 seconds. If your bot does not call answerPreCheckoutQuery in time, Telegram cancels the transaction.

Can I send a subscription with sendInvoice?

No. subscription_period is a parameter of createInvoiceLink. It must be 2592000 seconds (30 days), and the price can be at most 10,000 Stars.

How do I refund a Stars payment?

Call refundStarPayment with the user_id and the telegram_payment_charge_id from the successful_payment message.

Which commands must a paid bot support?

Telegram asks bots that sell digital goods to respond to /paysupport for payment issues. The go-live checklist also asks for an easy way to reach your terms (for example /terms) and support (for example /support).

Sources

Checked on 2026-10-28. Telegram changes the API often. AddThisBot is not affiliated with Telegram.

For builders

Want Stars payments without the work?

Two ways to work with us. Pick the one that fits.

Questions first? Message us in Telegram.