Payment Options for Telegram Bots: What Works in 2026
A client messaged me at 2 AM: "My bot takes orders but I still invoice manually β can you fix this by morning?" By 6 AM, their bot was processing payments through Stripe. Here is exactly how I did it β and how you can do the same in 30 minutes.
Before diving into code, let me show you every payment option available for Telegram bots β because the right choice depends on your business, not on what is easiest to implement.
| Payment Method | Setup Time | Transaction Fees | Best For | Complexity |
|---|---|---|---|---|
| Telegram Payments (native) | 15 min | 0% (Telegram) + provider fees | Simple stores, quick setup | Low |
| Stripe | 30 min | 2.9% + $0.30/txn | US businesses, cards, Apple Pay | Medium |
| PayPal | 30 min | 3.49% + $0.49/txn | PayPal users, international | Medium |
| Crypto (NOWPayments) | 20 min | 0.5% β 1% | Crypto-native audiences | LowβMedium |
| Manual invoice | 0 min | 0% (your time is the cost) | Testing only | None |
Telegram Payments (Native)
Telegram has a built-in payment system that works through third-party providers. The customer never leaves Telegram β the payment form opens as a native overlay.
Supported providers: Stripe, PayPal, YooMoney, Sberbank, and 10+ others.
Pros: - Zero friction β customer stays in Telegram - Native UI that feels like part of the app - No web view redirect - Telegram does not charge any fee (only the payment provider does)
Cons: - Limited customization of the payment form - Not all providers available in all countries - No subscription/recurring payments natively
Stripe
The most popular choice for US-based businesses. Stripe supports credit/debit cards, Apple Pay, Google Pay, ACH, and 135+ currencies.
Why Stripe wins for most bots: - Best documentation in the industry - Webhook-based architecture (perfect for bots) - Stripe Checkout handles PCI compliance for you - Supports refunds, subscriptions, and invoicing
PayPal
Essential if your customers prefer PayPal β especially in the US where 400+ million accounts are active. PayPal adds an extra login step, but for PayPal loyalists, it is a dealbreaker if missing.
Crypto (NOWPayments)
A niche but growing option. NOWPayments integrates easily and supports 100+ cryptocurrencies. Best for tech-savvy audiences or international customers who prefer crypto.
Which Should You Choose?
| Your Situation | Recommended | Why |
|---|---|---|
| US-based, selling to US customers | Stripe | Lowest friction, best ecosystem |
| International customers | Stripe + PayPal | Cover both card and PayPal users |
| Quick MVP, minimal setup | Telegram Payments + Stripe | Native feel, fast to implement |
| Crypto-native audience | Stripe + NOWPayments | Traditional + crypto options |
| Subscription products | Stripe | Only Stripe handles recurring well |
My recommendation for 90% of projects: start with Stripe via Telegram Payments. It gives you the native Telegram experience with Stripe's powerful backend. Add PayPal later if customer data shows demand.
π¬ Not sure which payment method fits your business? I have integrated all of them. Tell me about your store and I will recommend the best setup β bots from $500, delivered in 5-7 days β
Step-by-Step: Adding Stripe Payments to Your Telegram Bot
This is the exact code I use in production for Stripe integration. It takes about 30 minutes to set up from scratch.
Prerequisites
Before you start, you need: 1. A Stripe account (free at stripe.com) 2. Your Stripe API keys (test + production) 3. A Telegram bot token (from @BotFather) 4. Python 3.10+ with aiogram 3.x installed 5. A server with a public URL (for webhooks)
Step 1: Install Dependencies
1# Install required libraries
2pip install aiogram stripe aiohttpStep 2: Configure Stripe
1# stripe_config.py β Stripe configuration
2# Store keys in environment variables, NOT in code!
3
4import os
5import stripe
6
7# Test keys (for development)
8# Find them in Stripe Dashboard β Developers β API keys
9stripe.api_key = os.getenv("STRIPE_SECRET_KEY") # sk_test_...
10STRIPE_WEBHOOK_SECRET = os.getenv("STRIPE_WEBHOOK_SECRET") # whsec_...
11
12# Public URL of your server (needed for webhook)
13# In production replace with your domain
14WEBHOOK_URL = os.getenv("WEBHOOK_URL", "https://your-server.com/webhook/stripe")Step 3: Create a Payment Session
When the customer taps "Pay," the bot creates a Stripe Checkout Session and sends them a payment link.
1# payments.py β creating Stripe payment session
2
3import stripe
4from aiogram import Router, F
5from aiogram.types import CallbackQuery, InlineKeyboardButton
6from aiogram.utils.keyboard import InlineKeyboardBuilder
7
8router = Router()
9
10async def create_checkout_session(
11 user_id: int,
12 order_id: str,
13 items: list[dict],
14 bot_username: str,
15) -> str:
16 """
17 Creates Stripe Checkout Session for order payment.
18 Returns URL to redirect user to payment.
19
20 Parameters:
21 - user_id: Telegram ID of the buyer
22 - order_id: order number in our system
23 - items: list of items [{name, price, quantity}]
24 - bot_username: bot name (for return after payment)
25 """
26 # Build product list for Stripe
27 line_items = []
28 for item in items:
29 line_items.append({
30 "price_data": {
31 "currency": "usd", # Payment currency
32 "product_data": {
33 "name": item["name"], # Product name
34 },
35 # Stripe accepts price in cents (100 = $1.00)
36 "unit_amount": int(item["price"] * 100),
37 },
38 "quantity": item["quantity"], # Quantity
39 })
40
41 # Create payment session
42 session = stripe.checkout.Session.create(
43 payment_method_types=["card"], # Accept cards
44 line_items=line_items,
45 mode="payment", # One-time payment (not subscription)
46 # Return URL after successful payment
47 success_url=f"https://t.me/{bot_username}?start=payment_success_{order_id}",
48 # Return URL on payment cancellation
49 cancel_url=f"https://t.me/{bot_username}?start=payment_cancel_{order_id}",
50 # Metadata β link payment to our order
51 metadata={
52 "telegram_user_id": str(user_id),
53 "order_id": order_id,
54 },
55 )
56
57 return session.url # Payment URL
58
59
60@router.callback_query(F.data.startswith("pay:"))
61async def handle_payment(callback: CallbackQuery):
62 """
63 Handler for "Pay" button.
64 Callback data format: "pay:order_id"
65 """
66 order_id = callback.data.split(":")[1]
67
68 # TODO: get order items from database
69 # items = await db.get_order_items(order_id)
70 # Using test data for now:
71 items = [
72 {"name": "T-Shirt (Black, M)", "price": 29.99, "quantity": 1},
73 {"name": "Shipping", "price": 5.99, "quantity": 1},
74 ]
75
76 # Create payment session
77 payment_url = await create_checkout_session(
78 user_id=callback.from_user.id,
79 order_id=order_id,
80 items=items,
81 bot_username=callback.bot.username,
82 )
83
84 # Send button with payment link
85 builder = InlineKeyboardBuilder()
86 builder.row(InlineKeyboardButton(
87 text="π³ Pay with Card",
88 url=payment_url, # Stripe Checkout link
89 ))
90
91 await callback.message.edit_text(
92 text=f"π <b>Order #{order_id}</b>\n\n"
93 f"Total: $35.98\n\n"
94 f"Tap the button below to pay securely via Stripe:",
95 reply_markup=builder.as_markup(),
96 )
97 await callback.answer()Step 4: Handle Webhooks (Payment Confirmation)
Stripe sends a webhook when payment succeeds. Your bot listens for it and confirms the order.
1# webhook.py β handling webhook from Stripe
2
3from aiohttp import web
4import stripe
5import json
6
7# Webhook secret key (from Stripe Dashboard β Webhooks)
8WEBHOOK_SECRET = "whsec_your_webhook_secret_here"
9
10async def stripe_webhook_handler(request: web.Request) -> web.Response:
11 """
12 Receives webhook events from Stripe.
13 Stripe sends confirmation here after each payment.
14 """
15 # Get request body and signature
16 payload = await request.body()
17 sig_header = request.headers.get("Stripe-Signature")
18
19 try:
20 # Verify signature β ensure request is from Stripe
21 event = stripe.Webhook.construct_event(
22 payload, sig_header, WEBHOOK_SECRET
23 )
24 except ValueError:
25 # Invalid payload
26 return web.Response(status=400)
27 except stripe.error.SignatureVerificationError:
28 # Invalid signature β possibly fake request
29 return web.Response(status=400)
30
31 # Handle successful payment event
32 if event["type"] == "checkout.session.completed":
33 session = event["data"]["object"]
34
35 # Extract metadata from session
36 user_id = int(session["metadata"]["telegram_user_id"])
37 order_id = session["metadata"]["order_id"]
38 amount = session["amount_total"] / 100 # Convert from cents
39
40 # Update order status in database
41 # await db.update_order_status(order_id, "paid")
42
43 # Send confirmation to buyer via Telegram
44 # await bot.send_message(
45 # chat_id=user_id,
46 # text=f"β
Payment received! Order #{order_id}\n"
47 # f"Amount: ${amount:.2f}\n"
48 # f"We will ship your order soon!"
49 # )
50
51 print(f"β
Payment confirmed: order={order_id}, user={user_id}, amount=${amount}")
52
53 return web.Response(status=200)
54
55
56def setup_webhook_app(bot) -> web.Application:
57 """
58 Configures aiohttp application for receiving webhook.
59 Runs on the same server as the bot.
60 """
61 app = web.Application()
62 # Register webhook handler at /webhook/stripe
63 app.router.add_post("/webhook/stripe", stripe_webhook_handler)
64 return appStep 5: Test with Stripe Test Mode
Stripe provides test card numbers β no real money involved:
| Card Number | Scenario |
|---|---|
4242 4242 4242 4242 | Successful payment |
4000 0000 0000 0002 | Declined payment |
4000 0025 0000 3155 | Requires 3D Secure |
Use any future expiry date (12/34) and any 3-digit CVC.
Your first action: create a free Stripe account at stripe.com and grab your test API keys. That takes 5 minutes and unblocks everything else.
π¬ Need help setting up Stripe for your bot? I have integrated Stripe into 20+ Telegram bots. Get a working payment bot in 3 days β from $500 β
Adding PayPal to Your Telegram Bot
PayPal is the second most requested payment method after Stripe. Here is how to add it to your Telegram bot.
Why Add PayPal?
| Factor | Stripe | PayPal |
|---|---|---|
| User base | Card holders (everyone) | 400M+ PayPal accounts |
| Extra login step | No | Yes (PayPal login) |
| Transaction fees | 2.9% + $0.30 | 3.49% + $0.49 |
| Refund handling | API-based | API-based |
| Subscription support | Excellent | Good |
| International | 135+ currencies | 200+ markets |
| Setup complexity | Medium | Medium |
Step 1: Set Up PayPal Developer Account
- 1.Go to developer.paypal.com
- 2.Create a REST API app
- 3.Get your Client ID and Secret
- 4.Switch between Sandbox (testing) and Live (production)
Step 2: Create a PayPal Order
1# paypal_payments.py β creating order via PayPal API
2
3import aiohttp
4import os
5
6# PayPal keys (from Developer Dashboard)
7PAYPAL_CLIENT_ID = os.getenv("PAYPAL_CLIENT_ID")
8PAYPAL_SECRET = os.getenv("PAYPAL_SECRET")
9
10# Base URL (sandbox for testing, api-m for production)
11PAYPAL_BASE = "https://api-m.sandbox.paypal.com" # Test mode
12# PAYPAL_BASE = "https://api-m.paypal.com" # Production
13
14async def get_paypal_access_token() -> str:
15 """
16 Gets PayPal API access token.
17 Token lives 8 hours, needs refreshing.
18 """
19 async with aiohttp.ClientSession() as session:
20 # Authorization via Basic Auth
21 auth = aiohttp.BasicAuth(PAYPAL_CLIENT_ID, PAYPAL_SECRET)
22 async with session.post(
23 f"{PAYPAL_BASE}/v1/oauth2/token",
24 auth=auth,
25 data={"grant_type": "client_credentials"},
26 ) as resp:
27 data = await resp.json()
28 return data["access_token"]
29
30
31async def create_paypal_order(
32 order_id: str,
33 items: list[dict],
34 total_amount: float,
35) -> dict:
36 """
37 Creates order in PayPal and returns payment link.
38
39 Parameters:
40 - order_id: order number in our system
41 - items: list of items [{name, price, quantity}]
42 - total_amount: total order amount in dollars
43 """
44 token = await get_paypal_access_token()
45
46 # Build request body for PayPal
47 order_body = {
48 "intent": "CAPTURE", # Capture payment immediately
49 "purchase_units": [{
50 "reference_id": order_id, # Our order number
51 "amount": {
52 "currency_code": "USD",
53 # Split amount into items and shipping
54 "value": f"{total_amount:.2f}",
55 "breakdown": {
56 "item_total": {
57 "currency_code": "USD",
58 "value": f"{total_amount:.2f}",
59 }
60 }
61 },
62 # List of items in order
63 "items": [
64 {
65 "name": item["name"],
66 "unit_amount": {
67 "currency_code": "USD",
68 "value": f"{item['price']:.2f}",
69 },
70 "quantity": str(item["quantity"]),
71 }
72 for item in items
73 ],
74 }],
75 # Where to return user after payment
76 "application_context": {
77 "return_url": f"https://your-server.com/paypal/success?order={order_id}",
78 "cancel_url": f"https://your-server.com/paypal/cancel?order={order_id}",
79 }
80 }
81
82 async with aiohttp.ClientSession() as session:
83 async with session.post(
84 f"{PAYPAL_BASE}/v2/checkout/orders",
85 headers={
86 "Authorization": f"Bearer {token}",
87 "Content-Type": "application/json",
88 },
89 json=order_body,
90 ) as resp:
91 data = await resp.json()
92
93 # Look for payment link in response
94 for link in data.get("links", []):
95 if link["rel"] == "approve":
96 return {
97 "order_id": data["id"], # Order ID in PayPal
98 "approval_url": link["href"], # Payment link
99 }
100
101 raise Exception(f"PayPal error: {data}")Step 3: Capture the Payment (After Customer Approves)
1# paypal_capture.py β capturing payment after buyer approval
2
3async def capture_paypal_order(paypal_order_id: str) -> dict:
4 """
5 Captures payment after buyer approved it on PayPal page.
6 Called from webhook or callback.
7 """
8 token = await get_paypal_access_token()
9
10 async with aiohttp.ClientSession() as session:
11 async with session.post(
12 f"{PAYPAL_BASE}/v2/checkout/orders/{paypal_order_id}/capture",
13 headers={
14 "Authorization": f"Bearer {token}",
15 "Content-Type": "application/json",
16 },
17 ) as resp:
18 data = await resp.json()
19
20 # Check capture status
21 if data["status"] == "COMPLETED":
22 # Payment successful!
23 payer_email = data["payer"]["email_address"]
24 amount = data["purchase_units"][0]["payments"]["captures"][0]["amount"]["value"]
25
26 return {
27 "status": "completed",
28 "payer_email": payer_email,
29 "amount": float(amount),
30 }
31 else:
32 return {"status": data["status"], "error": "Payment not completed"}Stripe vs PayPal: My Recommendation
For most Telegram bots, I recommend Stripe as primary and PayPal as secondary. Here is why:
- Stripe has lower fees (2.9% vs 3.49%)
- Stripe Checkout works better inside Telegram's web view
- Stripe's webhook system is more reliable for bot integrations
- PayPal adds a login step that increases friction
Add PayPal only if: - Your analytics show 20%+ of customers attempt PayPal - You sell internationally where PayPal is dominant - Your product price is over $100 (PayPal's buyer protection is a trust signal)
I wrote a comprehensive comparison of all payment options in my Telegram bot payments integration overview section above. For e-commerce specific guidance, see my Telegram bot for e-commerce guide.
π¬ Want both Stripe and PayPal in your bot? I build payment integrations that handle both seamlessly β the customer chooses their preferred method at checkout. Get a quote β payment bots from $800 β
Telegram Native Payments: The Fastest Way to Accept Money
Telegram has its own payment system built into the Bot API. It is the fastest way to add payments β about 15 minutes of code β and the customer never leaves Telegram.
How Telegram Payments Work
- 1.Bot sends an invoice (a special message with product details and a "Pay" button)
- 2.Customer taps "Pay" β Telegram shows a native payment form
- 3.Customer enters card details (or selects a saved card)
- 4.Payment is processed by the provider (Stripe, PayPal, etc.)
- 5.Bot receives a successful\_payment event
The key difference from Stripe Checkout: the customer never leaves Telegram. The payment form is a native overlay, not a web page redirect.
Code: Sending an Invoice
1# telegram_payments.py β sending invoice via Telegram Payments API
2
3from aiogram import Router, F
4from aiogram.types import (
5 LabeledPrice, # For specifying price
6 InlineKeyboardButton,
7)
8from aiogram.utils.keyboard import InlineKeyboardBuilder
9
10router = Router()
11
12# Provider token β obtained from @BotFather
13# For Stripe: token from Stripe Dashboard β Settings β Payment methods
14PROVIDER_TOKEN = "YOUR_PROVIDER_TOKEN_FROM_BOTFATHER"
15
16@router.callback_query(F.data.startswith("invoice:"))
17async def send_invoice(callback):
18 """
19 Sends invoice to buyer via Telegram Payments.
20 Callback data format: "invoice:order_id"
21 """
22 order_id = callback.data.split(":")[1]
23
24 # Build price list (items + shipping)
25 prices = [
26 LabeledPrice(label="T-Shirt (Black, M)", amount=2999), # $29.99 in cents
27 LabeledPrice(label="Shipping", amount=599), # $5.99 in cents
28 ]
29
30 # Send invoice β Telegram will show native payment form
31 await callback.bot.send_invoice(
32 chat_id=callback.from_user.chat.id,
33
34 # Main product information
35 title="Order #1234", # Invoice title
36 description="T-Shirt (Black, M) + Shipping", # Description
37 payload=f"order_{order_id}", # Internal identifier
38 provider_token=PROVIDER_TOKEN, # Payment provider token
39 currency="usd", # Currency
40 prices=prices, # List of items
41
42 # Additional parameters (optional)
43 need_name=False, # Request name?
44 need_phone_number=False, # Request phone?
45 need_email=False, # Request email?
46 need_shipping_address=False, # Request shipping address?
47 is_flexible=False, # Shipping price depends on address?
48 start_parameter=f"pay_{order_id}", # Deep link parameter
49 )
50
51 await callback.answer("Invoice sent! Check your messages.")
52
53
54@router.pre_checkout_query()
55async def process_pre_checkout(pre_checkout_query):
56 """
57 Pre-checkout handler β Telegram asks "all ok?"
58 before charging.
59 Here you can verify stock, amount, etc.
60 """
61 # Check that item is in stock
62 # If all ok β confirm
63 await pre_checkout_query.answer(ok=True)
64
65 # If something is wrong β reject with error:
66 # await pre_checkout_query.answer(
67 # ok=False,
68 # error_message="Sorry, this item is out of stock."
69 # )
70
71
72@router.message(F.successful_payment)
73async def process_successful_payment(message):
74 """
75 Successful payment handler.
76 Called AFTER money is charged.
77 """
78 payment = message.successful_payment
79
80 # Extract payment data
81 order_payload = payment.invoice_payload # Our payload ("order_1234")
82 total_amount = payment.total_amount / 100 # Amount in dollars
83 currency = payment.currency # Currency
84 telegram_charge_id = payment.telegram_payment_id # Transaction ID in Telegram
85 provider_charge_id = payment.provider_payment_id # Transaction ID at provider
86
87 # Update order status in database
88 # await db.update_order_status(order_payload, "paid")
89
90 # Send confirmation to buyer
91 await message.answer(
92 f"β
<b>Payment successful!</b>\n\n"
93 f"Order: {order_payload}\n"
94 f"Amount: ${total_amount:.2f} {currency}\n\n"
95 f"We will process your order right away! π"
96 )Telegram Payments vs Stripe Checkout
| Feature | Telegram Payments | Stripe Checkout |
|---|---|---|
| Customer stays in Telegram | β Yes | β Redirects to web |
| Setup time | 15 min | 30 min |
| Saved cards | β Telegram remembers | β Stripe remembers |
| Customization | Limited | Full control |
| Refunds | Via Stripe API | Via Stripe API |
| Subscriptions | β No | β Yes |
| Analytics | Basic | Full Stripe Dashboard |
My recommendation: use Telegram Payments for simple, one-time purchases. Use Stripe Checkout for subscriptions, complex products, or when you need full control over the payment experience.
π¬ Want the fastest payment setup possible? Telegram Payments with Stripe as the provider gives you the best of both worlds β native Telegram UX with Stripe's backend. I can set it up in a day β from $500 β
Security Best Practices for Payment Bots
Processing payments in a Telegram bot means handling real money. Security is not optional β it is the foundation. Here is the checklist I follow on every payment bot project.
| # | Security Measure | Priority | What It Prevents |
|---|---|---|---|
| 1 | Never store card details | Critical | PCI compliance, data breaches |
| 2 | Verify webhook signatures | Critical | Fake payment confirmations |
| 3 | Use HTTPS everywhere | Critical | Man-in-the-middle attacks |
| 4 | Validate amounts server-side | High | Price manipulation |
| 5 | Rate limit payment endpoints | High | Abuse, DoS attacks |
| 6 | Log all transactions | High | Audit trail, dispute resolution |
| 7 | Use environment variables for keys | High | Accidental key exposure |
| 8 | Implement idempotency | Medium | Double charges |
| 9 | Set up fraud alerts | Medium | Suspicious transactions |
| 10 | Regular security audits | Medium | Emerging vulnerabilities |
Rule #1: Never Store Card Details
This is non-negotiable. Let Stripe, PayPal, or Telegram handle card data. Your bot should only store: - Payment provider transaction ID - Order amount and currency - Payment status (pending, completed, refunded) - Timestamp
Never store: card numbers, CVV, expiry dates, or full cardholder names. This is PCI DSS compliance 101.
Rule #2: Verify Webhook Signatures
Every payment provider signs their webhooks. Always verify the signature before processing:
1# Secure webhook verification from Stripe
2# ALWAYS verify signature before processing payment
3
4import stripe
5
6def verify_stripe_webhook(payload: bytes, sig_header: str, secret: str) -> dict:
7 """
8 Verifies webhook signature from Stripe.
9 If signature is invalid β raises exception.
10 Never process webhook without signature verification!
11 """
12 try:
13 event = stripe.Webhook.construct_event(
14 payload, sig_header, secret
15 )
16 return event
17 except stripe.error.SignatureVerificationError:
18 # Signature mismatch β possible forgery
19 raise ValueError("Invalid webhook signature β possible forgery!")Rule #4: Validate Amounts Server-Side
Never trust the amount sent from the client (the Telegram message). Always recalculate the total on your server:
1# Server-side order amount validation
2# Never trust amount from client β recalculate yourself
3
4async def validate_order_amount(order_id: str, claimed_total: float) -> bool:
5 """
6 Recalculates order amount on server and compares
7 with declared. Protection against price tampering.
8 """
9 # Get order items from DB
10 items = await db.get_order_items(order_id)
11
12 # Recalculate amount using DB prices (NOT from client!)
13 calculated_total = sum(
14 item.price * item.quantity # Price from our DB
15 for item in items
16 )
17
18 # Compare with declared amount
19 # Allow 1 cent tolerance (rounding)
20 return abs(calculated_total - claimed_total) < 0.01Rule #8: Implement Idempotency
If a webhook is delivered twice (it happens!), your bot should not charge the customer twice. Use the payment provider's transaction ID as a unique key:
1# Idempotency β protection against double charging
2# If webhook arrived twice β ignore second time
3
4async def handle_payment_webhook(transaction_id: str, order_id: str, amount: float):
5 """
6 Processes payment webhook with duplicate protection.
7 """
8 # Check β have we already processed this transaction?
9 existing = await db.get_transaction(transaction_id)
10 if existing:
11 # Already processed β ignore (don't charge again!)
12 print(f"β οΈ Duplicate webhook ignored: {transaction_id}")
13 return
14
15 # First time β process payment
16 await db.save_transaction(transaction_id, order_id, amount)
17 await db.update_order_status(order_id, "paid")
18 print(f"β
Payment processed: {transaction_id}")Review your bot against this checklist. If you are missing even one critical item (#1β#3), fix it before going live. A single security incident can destroy customer trust permanently.
Payment Bot Testing Checklist: 15 Things to Test Before Launch
I have launched 20+ payment bots. Every single one had at least one bug caught during testing that would have cost real money in production. Here is the exact checklist I use.
Happy Path (Successful Payments)
| # | Test Case | How to Test | Expected Result |
|---|---|---|---|
| 1 | Successful card payment | Use test card 4242... | Order confirmed, receipt sent |
| 2 | Payment with correct amount | Compare bot total vs Stripe total | Amounts match exactly |
| 3 | Webhook received | Check server logs after payment | Webhook logged and processed |
| 4 | Order status updated | Check database after payment | Status = "paid" |
| 5 | Confirmation message sent | Check Telegram after payment | Customer receives confirmation |
Error Path (Failed Payments)
| # | Test Case | How to Test | Expected Result |
|---|---|---|---|
| 6 | Declined card | Use test card 4000...0002 | Friendly error message, no charge |
| 7 | Insufficient funds | Use test card 4000...9995 | Friendly error message |
| 8 | 3D Secure required | Use test card 4000...3155 | 3DS flow works, payment completes |
| 9 | Customer cancels payment | Tap cancel on Stripe page | Return to bot, order preserved |
| 10 | Network timeout | Simulate slow connection | Bot handles gracefully, no crash |
Edge Cases
| # | Test Case | How to Test | Expected Result |
|---|---|---|---|
| 11 | Double webhook delivery | Replay webhook from Stripe dashboard | No double charge (idempotency) |
| 12 | Price change during checkout | Change price in DB while customer pays | Server-side validation catches mismatch |
| 13 | Out of stock after cart | Remove product after add-to-cart | Error at checkout, not at payment |
| 14 | Refund processing | Issue refund via Stripe, check bot | Order status = "refunded", customer notified |
| 15 | Concurrent payments | Two users pay for last item simultaneously | Only one succeeds, other gets error |
Test Card Numbers (Stripe)
| Card Number | Scenario | What Should Happen |
|---|---|---|
4242 4242 4242 4242 | Success | Payment completes |
4000 0000 0000 0002 | Declined | Error message shown |
4000 0000 0000 9995 | Insufficient funds | Error message shown |
4000 0025 0000 3155 | 3D Secure | 3DS popup, then success |
4000 0000 0000 0341 | Failed (attach) | Attach error |
Do not skip testing. I once caught a bug where the bot showed the wrong total after a cart modification β the customer would have been charged $45 instead of $35. One hour of testing saved a refund, a support ticket, and a bad review.
Common Payment Integration Errors (And How to Fix Them)
These are the errors I see developers hit most often when integrating payments into Telegram bots. Each one has a simple fix β if you know where to look.
| Error | Cause | Fix |
|---|---|---|
| "Provider token not found" | Wrong token or not configured in BotFather | Re-configure payments in @BotFather |
| Webhook not receiving events | Wrong URL or HTTPS not configured | Use ngrok for testing, verify URL in Stripe |
| "No such price" | Amount in wrong format | Use cents: $10.00 = 1000, not 10 |
| Payment form not showing | Provider token is for wrong country | Match provider country to your Stripe account |
| "Invoice not found" | Invoice expired (24h limit) | Create fresh invoice when customer is ready |
| Double charges | No idempotency check | Use transaction ID as unique key |
| Refund not working | Using test key in production | Check you are using live keys |
| Currency mismatch | Bot sends USD, Stripe expects EUR | Match currency in bot code and Stripe settings |
Error #1: "Provider Token Not Found"
This is the most common error for beginners. It means Telegram cannot find your payment provider configuration.
Fix: 1. Open @BotFather 2. Select your bot β Payments 3. Choose a provider (e.g., Stripe) 4. Follow the setup flow 5. Copy the provider token 6. Use it in your code
Error #3: Amount in Wrong Format
Stripe and Telegram Payments both expect amounts in the smallest currency unit:
- USD: $10.00 β 1000 (cents)
- EUR: β¬10.00 β 1000 (cents)
- JPY: Β₯1000 β 1000 (yen, no decimals)
Common mistake: sending 10.00 instead of 1000. This results in a $0.10 charge instead of $10.00.
Error #6: Double Charges
If your webhook handler does not check for duplicates, a retry (Stripe retries failed webhooks for up to 3 days) will process the payment again.
Fix: store the Stripe event ID in your database and check before processing:
1# Protection against duplicate webhook processing
2# Stripe may send webhook multiple times on retry
3
4async def handle_webhook(event_id: str, event_type: str, data: dict):
5 """
6 Processes webhook with duplicate protection.
7 """
8 # Check β have we seen this event before?
9 if await db.event_exists(event_id):
10 # Already processed β skip
11 return {"status": "already_processed"}
12
13 # Save event ID to DB (before processing!)
14 await db.save_event(event_id, event_type)
15
16 # Now safe to process
17 if event_type == "checkout.session.completed":
18 await process_payment(data)
19
20 return {"status": "processed"}Pro tip: always test with Stripe's test mode first. Switch to live keys only after all 15 test cases pass. I have seen developers accidentally charge real customers during testing because they used live keys too early.
Cost Comparison: Which Payment Method Is Cheapest for Your Volume?
Transaction fees add up. Here is exactly how much each payment method costs at different monthly volumes β so you can make an informed decision.
Fee Comparison by Monthly Volume
| Monthly Revenue | Stripe Fees | PayPal Fees | Telegram + Stripe | Savings vs PayPal |
|---|---|---|---|---|
| $1,000 | $59 | $74 | $59 | $15/month |
| $5,000 | $245 | $324 | $245 | $79/month |
| $10,000 | $490 | $649 | $490 | $159/month |
| $25,000 | $1,025 | $1,522 | $1,025 | $497/month |
| $50,000 | $1,950 | $2,945 | $1,950 | $995/month |
| $100,000 | $3,900 | $5,890 | $3,900 | $1,990/month |
Stripe: 2.9% + $0.30/transaction. PayPal: 3.49% + $0.49/transaction. Assuming average transaction of $35.
The Hidden Cost: Failed Payments
Not all payment attempts succeed. Here is the real cost including failed attempts:
| Provider | Success Rate | Effective Fee (per successful txn) |
|---|---|---|
| Stripe | 95% | 3.05% + $0.32 |
| PayPal | 92% | 3.79% + $0.53 |
| Telegram Payments (Stripe) | 97% | 2.99% + $0.31 |
Telegram Payments has the highest success rate because the customer never leaves the app β no redirect means fewer abandoned payments.
Development Cost Comparison
| Approach | Dev Time | Dev Cost | Monthly Maintenance |
|---|---|---|---|
| Stripe Checkout (redirect) | 2β3 days | $500 β $1,000 | $0 (Stripe handles it) |
| Stripe + Telegram Payments | 3β5 days | $800 β $1,500 | $0 |
| PayPal integration | 3β5 days | $800 β $1,500 | $0 |
| Both Stripe + PayPal | 5β7 days | $1,200 β $2,500 | $0 |
| Custom payment flow | 2β4 weeks | $3,000 β $8,000 | $100 β $300 |
My Recommendation by Business Type
| Business Type | Recommended Setup | Why |
|---|---|---|
| Small store (< $5K/month) | Telegram Payments + Stripe | Lowest friction, fastest setup |
| Medium store ($5Kβ$50K/month) | Stripe Checkout + PayPal option | Flexibility, better analytics |
| Subscription business | Stripe Subscriptions | Only Stripe handles recurring well |
| International sales | Stripe + PayPal + Crypto | Cover all payment preferences |
| High-ticket ($500+) | Stripe + PayPal (buyer protection) | Trust signal for expensive items |
Total Cost of Ownership (First Year)
| Component | Stripe Only | Stripe + PayPal | All Three |
|---|---|---|---|
| Development | $800 β $1,500 | $1,200 β $2,500 | $1,500 β $3,000 |
| Transaction fees ($10K/month) | $5,880/year | $5,880/year | $5,880/year |
| Hosting | $120/year | $120/year | $120/year |
| Maintenance | $0 | $0 | $0 |
| Total Year 1 | $6,800 β $7,500 | $7,200 β $8,500 | $7,500 β $9,000 |
The difference between payment methods is small compared to the revenue they enable. Do not overthink it β start with Stripe, add PayPal if customers ask for it.
I cover the full picture of bot costs β including payment integration β in my telegram bot development cost guide. And if you want to see how payments work in a real e-commerce context, check my Telegram bot for e-commerce case study.
π¬ Ready to add payments to your Telegram bot? I have integrated Stripe, PayPal, and Telegram Payments into 20+ production bots. Describe your product and I will recommend the best payment setup β bots from $500, delivered in 5-7 days β
Frequently Asked Questions
Answers to the most popular questions about telegram bot payments integration
