Your App LogoYOUR APP EXPERTYAE
    • Services
    • About
    • Portfolio
    • Blog
    • FAQ
    • Build Your App
    1. Home
    2. Blog
    3. Telegram bot payments with Stripe: the production integration guide
    Bots & Messaging

    Telegram bot payments with Stripe: the production integration guide

    How to wire Stripe into a Telegram bot the right way — invoice flows, webhook idempotency, refund handling, and the parts the docs don't tell you.

    YAEL Engineering·10 Mar 2026·8 min read·1,505 words
    On this page
    • The two integration paths (you need to pick)
    • Path 1: Telegram-native invoice flow
    • Bug #1 — the bot restart during pre_checkout_query
    • Bug #2 — duplicate successful_payment processing
    • The Stripe webhook problem
    • Path 2: Stripe Checkout link inside Telegram
    • Refunds — the gotcha nobody mentions
    • Test mode
    • The deployment checklist
    • FAQ
    • Can I use Telegram payments without Stripe?
    • Does Telegram take a cut?
    • What about Telegram Stars / TON?
    • Can I do subscriptions through Telegram Payments natively?
    • What's the minimum charge amount?
    • Can users pay with Apple Pay / Google Pay?
    • How do refunds work?
    • What happens if my bot is offline when payment completes?

    Telegram natively supports payments through Stripe, Paddle, and other providers — but the on-ramp is undocumented in the parts that matter most. The official docs cover the happy path: send an invoice, the user pays, you receive a successful_payment update. They do not cover refunds, subscriptions, dispute handling, idempotent webhook processing across both Telegram's update stream and Stripe's webhook stream, or what happens when the bot restarts mid-checkout. This is the playbook we use to ship Telegram bot payments to production for real businesses.

    The two integration paths (you need to pick)

    Telegram offers two payment models. The choice is not interchangeable.

    Telegram Payments 2.0 (recommended for most cases). Telegram acts as the checkout UI. You call sendInvoice, Telegram presents the payment sheet, your user enters card details in Telegram, and Telegram charges through your connected Stripe account. You get a successful_payment update via the bot. Stripe also fires its own charge.succeeded and payment_intent.succeeded webhooks.

    Direct Stripe Checkout link. You send the user a Stripe Checkout URL. They open it in their browser, pay, get redirected to a success page you control. Telegram has no part in the payment flow other than delivering the link.

    Path 1 has the smoothest UX (everything happens in Telegram) but only supports one-off payments — no subscriptions, no usage-based billing.

    Path 2 is uglier (the user leaves Telegram) but supports the full Stripe surface — subscriptions, customer portals, Stripe-managed retries.

    For a tip jar, content unlock, or one-time product, use Path 1. For a SaaS subscription with monthly billing, use Path 2.

    Path 1: Telegram-native invoice flow

    The minimum viable code:

    ts
    // src/bots/telegram/commands/buy.ts
    import { Telegraf } from "telegraf";
    
    const bot = new Telegraf(process.env.TELEGRAM_BOT_TOKEN!);
    
    bot.command("buy", async (ctx) => {
      await ctx.replyWithInvoice({
        title: "Premium Access",
        description: "30 days of premium features",
        payload: JSON.stringify({
          userId: ctx.from.id,
          sku: "premium_30d",
          nonce: crypto.randomUUID(),
        }),
        provider_token: process.env.TELEGRAM_STRIPE_PROVIDER_TOKEN!,
        currency: "USD",
        prices: [{ label: "Premium Access (30d)", amount: 990 }], // $9.90
      });
    });
    
    // pre_checkout_query: last chance to reject before charging
    bot.on("pre_checkout_query", async (ctx) => {
      const payload = JSON.parse(ctx.preCheckoutQuery.invoice_payload);
      const ok = await isStillAvailable(payload.sku);
      await ctx.answerPreCheckoutQuery(ok, ok ? undefined : "Out of stock");
    });
    
    // successful_payment: charge went through, grant the entitlement
    bot.on("message", async (ctx, next) => {
      const msg = ctx.message;
      if (!("successful_payment" in msg)) return next?.();
      const payload = JSON.parse(msg.successful_payment.invoice_payload);
      await grantEntitlement(payload);
      await ctx.reply("✓ Premium unlocked. Enjoy.");
    });
    
    bot.launch();

    This works. It is also wrong for production. Two bugs hiding in plain sight.

    Bug #1 — the bot restart during pre_checkout_query

    The pre_checkout_query event must be answered within 10 seconds. If your bot is restarting, deploying, or otherwise unavailable, Telegram aborts the payment. Worse, Telegram does not redeliver pre_checkout_query — the user just sees a failure.

    Mitigation: keep pre_checkout_query handling as fast as possible. No external API calls. No database queries that can hang. The "is this still available" check should be a single indexed lookup. If you genuinely need to do slow work, do it asynchronously and pre-warm the answer at sendInvoice time.

    Bug #2 — duplicate successful_payment processing

    Telegram redelivers updates on bot restart if you haven't acknowledged them yet. The same successful_payment can land in your handler twice. Without idempotency, the user gets two entitlements for one payment.

    The fix is the same idempotency pattern from webhook idempotency is the bug most teams ship, keyed on provider_payment_charge_id from the successful_payment object:

    ts
    async function handleSuccessfulPayment(msg: SuccessfulPaymentMessage) {
      const chargeId = msg.successful_payment.provider_payment_charge_id;
      const result = await db.transaction(async (tx) => {
        const inserted = await tx.execute(
          `insert into processed_events (id, source, type, payload)
           values ($1, 'telegram', 'successful_payment', $2)
           on conflict (id) do nothing
           returning id`,
          [chargeId, msg],
        );
        if (inserted.length === 0) return "duplicate";
        await grantEntitlement(JSON.parse(msg.successful_payment.invoice_payload));
        return "processed";
      });
      return result;
    }

    The Stripe webhook problem

    When using Path 1, Stripe also sends webhooks to your endpoint for the same charge. You now have two event streams for one payment: Telegram's successful_payment update and Stripe's charge.succeeded webhook. They can arrive in either order.

    Two options.

    Option A — primary on Telegram, ignore Stripe. Your provider_token ties the Stripe charge to your account. Stripe charges. Telegram tells you about it. You grant the entitlement. You ignore Stripe's webhook for these charges. Simpler.

    Option B — primary on Stripe, ignore Telegram. You react only to Stripe webhooks. Your payment_intent metadata carries the Telegram chat id, so you can DM the user from the Stripe webhook handler. More work but better fits a system that already has Stripe-driven billing.

    For pure Telegram-native bots, Option A. For bots that share infrastructure with a SaaS, Option B.

    Path 2: Stripe Checkout link inside Telegram

    If you need subscriptions, you can't use Path 1. Use Stripe Checkout instead:

    ts
    bot.command("subscribe", async (ctx) => {
      const session = await stripe.checkout.sessions.create({
        mode: "subscription",
        line_items: [{ price: process.env.STRIPE_PRICE_PREMIUM_MONTHLY!, quantity: 1 }],
        success_url: `https://yourapp.com/telegram/success?token={CHECKOUT_SESSION_ID}`,
        cancel_url: `https://yourapp.com/telegram/cancel`,
        client_reference_id: String(ctx.from.id),
        subscription_data: {
          metadata: { telegram_user_id: String(ctx.from.id) },
        },
      });
      await ctx.reply(
        `Subscribe to Premium: ${session.url}`,
        { reply_markup: { inline_keyboard: [[{ text: "Subscribe →", url: session.url! }]] } },
      );
    });

    The Telegram chat id is in client_reference_id and the subscription metadata. Your Stripe webhook handler reads it and DMs the user via the Bot API on customer.subscription.created.

    ts
    // In your Stripe webhook handler
    case "customer.subscription.created": {
      const sub = event.data.object as Stripe.Subscription;
      const telegramUserId = sub.metadata.telegram_user_id;
      if (telegramUserId) {
        await telegramBot.telegram.sendMessage(
          Number(telegramUserId),
          "✓ Subscription active. /premium to see commands.",
        );
      }
      await upsertSubscription(sub);
      break;
    }

    Refunds — the gotcha nobody mentions

    Telegram-native payments (Path 1) support refunds only via your Stripe dashboard. Refunding fires Stripe's charge.refunded webhook — but Telegram does not deliver a corresponding update to your bot.

    If you don't handle Stripe's refund webhook, the user's entitlement stays granted after the refund. Customer-facing chaos.

    Always handle charge.refunded even in Path 1:

    ts
    case "charge.refunded": {
      const charge = event.data.object as Stripe.Charge;
      // Look up the entitlement by the Telegram charge id we stored.
      await revokeEntitlementByChargeId(charge.id);
      // Optionally DM the user via Telegram
      const userId = await getUserIdByChargeId(charge.id);
      if (userId) {
        await telegramBot.telegram.sendMessage(
          Number(userId),
          "Your payment was refunded. Access removed.",
        );
      }
      break;
    }

    We had to fix this in production three times across customer integrations. It's not in the Telegram docs anywhere. Now you know.

    Test mode

    Telegram's test environment uses a separate provider_token prefixed with TEST:. Stripe's test mode uses test keys. The two are independent — you need a test provider_token from @BotFather and a test Stripe API key.

    For local development:

    env
    TELEGRAM_BOT_TOKEN=...
    TELEGRAM_STRIPE_PROVIDER_TOKEN=TEST:284685063:...
    STRIPE_SECRET_KEY=sk_test_...
    STRIPE_WEBHOOK_SECRET=whsec_...

    Use stripe listen --forward-to localhost:3000/api/stripe/webhook to forward Stripe webhooks to your local server. Use ngrok or a similar tunnel to expose your bot to Telegram for setWebhook.

    The deployment checklist

    Before flipping the switch in production:

    • [ ] Bot webhook URL set via setWebhook (not polling) for reliability under load
    • [ ] processed_events table created and indexed
    • [ ] Stripe webhook endpoint registered with the correct signing secret
    • [ ] charge.refunded handler in place
    • [ ] payment_intent.payment_failed handler if doing card-based subscriptions
    • [ ] Pre-checkout availability check is <100ms
    • [ ] Idempotency on every successful_payment and Stripe webhook
    • [ ] Sentry or equivalent on both bot errors and webhook errors
    • [ ] Hard rate-limit on the /buy command to prevent abuse

    Building a Telegram bot with payments?

    We've shipped Telegram bots with Stripe, refund flows, subscription portals, and abuse-resistant rate limiting.

    See Telegram service

    FAQ

    Can I use Telegram payments without Stripe?

    Yes — Telegram supports Stripe, Paddle, Yandex.Money, Tranzzo, and several regional providers. The architecture is identical; only the provider_token changes. For most builders Stripe is the easiest globally.

    Does Telegram take a cut?

    Telegram does not. You pay only the Stripe processing fee. This makes Telegram payments meaningfully cheaper than App Store / Play Store for digital goods if your customer base is on Telegram.

    What about Telegram Stars / TON?

    Stars are Telegram's internal currency for digital goods in the Telegram Mini App ecosystem. Different system from card payments. If you're building a Telegram Mini App, Stars is the right call for in-app purchases. For straight bot commerce, Stripe via Path 1.

    Can I do subscriptions through Telegram Payments natively?

    No. Telegram's native payments are one-off only. Subscriptions require Path 2 (Stripe Checkout link).

    What's the minimum charge amount?

    Stripe enforces a $0.50 minimum in USD (varies by currency). Telegram's invoice flow respects whatever Stripe enforces.

    Can users pay with Apple Pay / Google Pay?

    Yes — Telegram surfaces Apple/Google Pay automatically when the user's device supports it. You don't need to configure anything bot-side.

    How do refunds work?

    Initiate from the Stripe dashboard or API. Stripe fires charge.refunded. Your webhook handler revokes the entitlement (Telegram does not deliver a refund update to the bot itself).

    What happens if my bot is offline when payment completes?

    Telegram queues updates and redelivers when the bot comes back online. Idempotency on provider_payment_charge_id prevents double-processing.

    TagsTelegramStripeBotsPaymentsWebhooks
    ServiceTelegram Bot DevelopmentStripe Integration
    PreviousWebhook idempotency: the bug most teams shipNext WhatsApp Business API: direct vs BSP — the real cost in 2026

    Keep reading

    Bots & MessagingDiscord bot + Stripe paid roles: the full architectureHow to grant Discord roles based on Stripe subscriptions — webhook flow, role syncing, churn handling, and the patterns that survive Discord's rate limits.8 min readBots & MessagingWhatsApp Business API: direct vs BSP — the real cost in 2026Direct Cloud API vs a Business Solution Provider — the actual cost breakdown, the parts BSPs hide, and when each model wins.9 min readArchitectureWebhook idempotency: the bug most teams shipWhy webhook handlers double-charge, double-grant, and double-cancel — and the three-line database pattern that fixes all of it.8 min read
    On this page
    • The two integration paths (you need to pick)
    • Path 1: Telegram-native invoice flow
    • Bug #1 — the bot restart during pre_checkout_query
    • Bug #2 — duplicate successful_payment processing
    • The Stripe webhook problem
    • Path 2: Stripe Checkout link inside Telegram
    • Refunds — the gotcha nobody mentions
    • Test mode
    • The deployment checklist
    • FAQ
    • Can I use Telegram payments without Stripe?
    • Does Telegram take a cut?
    • What about Telegram Stars / TON?
    • Can I do subscriptions through Telegram Payments natively?
    • What's the minimum charge amount?
    • Can users pay with Apple Pay / Google Pay?
    • How do refunds work?
    • What happens if my bot is offline when payment completes?

    YOUR APP EXPERT LTD

    71-75 Shelton Street, LONDON WC2H 9JQ, UK

    +44 20 1234 5678

    [email protected]

    Quick Links

    • Services
    • About Us
    • Portfolio
    • Blog
    • Contact

    Stay Connected

    Newsletter

    Stay updated with our latest innovations and insights.

    © 2026 YOUR APP EXPERT LTD. All rights reserved.

    Engineering the Future of Technology