Skip to content

Guides

Recipe: Telegram bot

Sell bot commands, a role or a private group. Each user gets their own invoice; a signed webhook unlocks them.

View as Markdown

The bot never sees money. On /buy it sends the user their own invoice link; your server hears invoice.fulfilled and unlocks. Every paid command asks Perly Pay, so a restart loses nothing. Discord works the same way, with a role.

With an assistant: the sell-bot-access skill of the Perly Pay plugin for Claude Code does this recipe in your project — the plugin and its example, examples/telegram-bot (MIT), are on their way. Until then, any assistant can read the same skill from /.well-known/skills/perly-pay-sell-bot-access/SKILL.md.

How it works

  1. 1

    A product and price

    One-time or lifetime for "forever", fixed term for passes, subscription for memberships renewed by invoice.

  2. 2

    A customer per chat user

    external_id: "tg:<user id>". Ask for an email only for subscriptions: renewals go there.

  3. 3

    /buy

    An invoice with the chat id in metadata, sent as a button. One invoice per user per day, by idempotency key.

  4. 4

    Unlock

    On invoice.fulfilled, read the chat id from metadata and answer: a message, a one-time invite link, a role.

  5. 5

    Paid commands

    entitlements.check({ customer_external_id: "tg:<id>", product }).

/buy
const invoice = await pay.invoices.create(
  { line_items: [{ price: PRICE }], customer: customer.id, metadata: { chat_id: String(chatId) } },
  { idempotencyKey: `buy-${userId}-${today}` },
);
await tg("sendMessage", { chat_id: chatId, text: "Pay:", reply_markup: { inline_keyboard: [[{ text: "Pay", url: invoice.url }]] } });

Run the example

  1. 1

    Install

    npm install in examples/telegram-bot (Node 20.12+).

  2. 2

    Key

    Copy .env.example to .env and put a test key (sk_test_…) in PERLY_PAY_SECRET_KEY.

  3. 3

    Set up

    npm run setup makes the product and price with your key and prints their ids for .env.

  4. 4

    Run

    npm start (or npm run dev), as its README says.

  5. 5

    Pay without a wallet

    Message the bot /buy, open Pay and press Simulate payment: the bot says "Payment confirmed" and /secret answers.

Pitfalls

  • One shared payment link for everyone: you cannot tell who paid. An invoice per user, with the chat id.
  • Unlocking when the user taps "I paid". Only the webhook or entitlements.check unlocks.
  • Access kept in the bot's memory. It is gone after a restart; ask Perly Pay.