Build a Telegram Payment Bot with VietQR & AgentPay

2026-09-24 · AgentPay VN

telegramvietqrpaymentspythonagentpay

The Problem: Running a Telegram Business Without Payment Integration

Imagine you're selling online courses through a Telegram bot. A student messages: "How do I pay?" Right now, you're copying a bank account number into chat, hoping they'll transfer the right amount with the correct reference. You wait hours—sometimes days—to manually confirm payment. Half your customers get confused and abandon the purchase. Your dream of automated digital product sales hits a wall.

This is the daily reality for thousands of Vietnamese creators, service providers, and small businesses trying to monetize on Telegram. You have an audience. You have a product. But payment collection feels stuck in 2015.

Enter AgentPay VN: an open-source Python SDK that lets your Telegram bot generate VietQR payment links in seconds, automatically confirm settlements, and continue conversations without you lifting a finger. No merchant account overhead. No money sitting in escrow. Just direct transfers to your bank account.

Why VietQR + Telegram = Perfect Match for Vietnamese Creators

VietQR is ubiquitous in Vietnam—every bank app supports it. Telegram's API is bulletproof and cost-free. Together, they're unstoppable.

Here's why AgentPay VN solves this uniquely:

Getting Started: Installation & Setup

Install the SDK

pip install agentpay-vn

That's your first step. The SDK is lightweight (~50KB uncompressed) and depends only on standard HTTP libraries.

Set Up MCP Server (Optional but Powerful)

If you're using Claude or planning to delegate payment logic to an AI agent:

pip install agentpay-mcp

Then configure Claude's MCP settings (e.g., in your .cursor/rules or Claude config):

{
  "mcpServers": {
    "agentpay-vn": {
      "command": "python",
      "args": ["-m", "agentpay.mcp"],
      "env": {
        "AGENTPAY_MERCHANT_ID": "your_merchant_id",
        "AGENTPAY_API_KEY": "your_api_key"
      }
    }
  }
}

Once configured, Claude can natively call AgentPay functions—perfect for building conversational payment flows.

Real-World Walkthrough: A Telegram Course-Selling Bot

Let's build a concrete example: TechCourse Bot. A creator sells Python courses ($15–$50 each) via Telegram. When someone messages /buy, the bot:

  1. Asks which course
  2. Generates a VietQR payment link
  3. Sends the checkout link
  4. Waits for settlement confirmation
  5. Delivers course access automatically

Step 1: Create a Payment Request

from agentpay_vn import AgentPay
import json

# Initialize the SDK
agent_pay = AgentPay(
    merchant_id="YOUR_MERCHANT_ID",
    api_key="YOUR_API_KEY"
)

# User buys the "Advanced Python" course for 199,000 VND
payment = agent_pay.create_payment_request(
    amount_vnd=199000,
    order_id="order_adv_python_001",
    description="Advanced Python Course - TechCourse Bot",
    customer_phone="0912345678",
    metadata={
        "course_id": "adv_python",
        "customer_name": "Nguyen Van A",
        "expires_in_minutes": 30
    }
)

print(f"Payment Request Created:")
print(f"  Order ID: {payment['order_id']}")
print(f"  Amount: {payment['amount_vnd']:,} VND")
print(f"  QR Code URL: {payment['qr_code_url']}")
print(f"  Checkout URL: {payment['checkout_url']}")
# Output:
# Payment Request Created:
#   Order ID: order_adv_python_001
#   Amount: 199,000 VND
#   QR Code URL: https://agentpay.servicesai.vn/qr/abc123xyz
#   Checkout URL: https://checkout.agentpay.vn/abc123xyz

Line-by-line: - create_payment_request() takes the amount in VND (Vietnamese Dong), a unique order ID, and optional metadata. - It returns a response with qr_code_url (for inline display) and checkout_url (for sharing via link). - The QR points directly at your bank account—AgentPay is transparent infrastructure.

Step 2: Send to Telegram User

import logging
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
from telegram.ext import Application, CommandHandler, ContextTypes

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

async def buy_course(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """Handle /buy command in Telegram."""

    user = update.message.from_user
    course_id = "adv_python"
    amount_vnd = 199000
    order_id = f"order_{course_id}_{user.id}_{int(time.time())}"

    # Create payment request
    payment = agent_pay.create_payment_request(
        amount_vnd=amount_vnd,
        order_id=order_id,
        description=f"Course: Advanced Python",
        customer_phone=user.username or "unknown",
        metadata={"course_id": course_id, "user_id": user.id}
    )

    # Build inline keyboard with payment button
    keyboard = [
        [
            InlineKeyboardButton(
                "💳 Pay 199,000 VND",
                url=payment['checkout_url']
            )
        ]
    ]
    reply_markup = InlineKeyboardMarkup(keyboard)

    # Send message with QR code image and payment link
    await update.message.reply_photo(
        photo=payment['qr_code_url'],
        caption=(
            f"🎓 *Advanced Python Course*\n\n"
            f"💰 Price: 199,000 VND\n"
            f"⏱️ Link expires in 30 minutes\n\n"
            f"Scan the QR or tap the button below 👇"
        ),
        reply_markup=reply_markup,
        parse_mode='Markdown'
    )

    # Store order in memory/database for settlement tracking
    context.user_data['pending_order'] = {
        'order_id': order_id,
        'course_id': course_id,
        'user_id': user.id
    }
    logger.info(f"Payment link sent to {user.id} for order {order_id}")

async def main():
    """Start the bot."""
    app = Application.builder().token("YOUR_TELEGRAM_BOT_TOKEN").build()
    app.add_handler(CommandHandler("buy", buy_course))
    await app.run_polling()

if __name__ == '__main__':
    import asyncio
    asyncio.run(main())

What happens: User taps the button or scans the QR. Their bank app opens with pre-filled details. They confirm. Money lands in your account.

Step 3: Wait for Settlement & Auto-Deliver

import asyncio
from datetime import datetime

async def check_settlement(order_id: str, max_wait_seconds: int = 1800):
    """Poll for payment settlement (30-minute timeout)."""

    start_time = datetime.now()
    poll_interval = 10  # Check every 10 seconds

    while (datetime.now() - start_time).total_seconds() < max_wait_seconds:
        settlement = agent_pay.await_settlement(
            order_id=order_id,
            timeout_seconds=poll_interval
        )

        if settlement['status'] == 'settled':
            logger.info(f"✅ Payment confirmed for {order_id}")
            logger.info(f"   Settled amount: {settlement['amount_vnd']:,} VND")
            logger.info(f"   Settled at: {settlement['settled_at']}")
            return settlement

        elif settlement['status'] == 'pending':
            logger.info(f"⏳ Waiting for settlement: {order_id}")
            await asyncio.sleep(poll_interval)

        else:  # 'expired', 'failed', 'cancelled'
            logger.warning(f"❌ Order {order_id} has status: {settlement['status']}")
            return settlement

    logger.error(f"Timeout waiting for settlement: {order_id}")
    return None

async def process_settlement(user_id: int, course_id: str, order_id: str, context: ContextTypes.DEFAULT_TYPE):
    """Background task to monitor payment and deliver course."""

    try:
        settlement = await check_settlement(order_id)

        if settlement and settlement['status'] == 'settled':
            # Payment confirmed!
            # 1. Generate course access (e.g., unique enrollment link)
            enrollment_link = f"https://courses.example.com/enroll/{course_id}?token=unique_token_123"

            # 2. Send to user
            await context.bot.send_message(
                chat_id=user_id,
                text=(
                    f"🎉 *Payment successful!*\n\n"
                    f"Your course access is ready:\n"
                    f"🔗 {enrollment_link}\n\n"
                    f"Questions? Reply to this message."
                ),
                parse_mode='Markdown'
            )

            # 3. Log to your system
            logger.info(f"Course delivered to user {user_id} for {order_id}")

    except Exception as e:
        logger.error(f"Settlement check failed: {e}")
        await context.bot.send_message(
            chat_id=user_id,
            text="⚠️ Payment verification issue. Please contact support."
        )

# In your /buy handler, spawn the background task:
# asyncio.create_task(process_settlement(user.id, course_id, order_id, context))

The flow: After the user pays, your bot polls await_settlement() every 10 seconds. When the bank confirms the transfer, status changes to 'settled'. You immediately deliver the course—no manual intervention.

Advanced Tips & Patterns

1. Handle Multiple Courses with Dynamic Pricing

COURSES = {
    "python_basics": {"name": "Python Basics", "price_vnd": 99000},
    "adv_python": {"name": "Advanced Python", "price_vnd": 199000},
    "web_dev": {"name": "Web Dev Masterclass", "price_vnd": 349000}
}

async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """Show course list with /start."""
    keyboard = [
        [InlineKeyboardButton(f"{data['name']} ({data['price_vnd']:,} VND)", 
                            callback_data=f"buy_{course_id}")]
        for course_id, data in COURSES.items()
    ]
    await update.message.reply_text(
        "📚 Choose a course:",
        reply_markup=InlineKeyboardMarkup(keyboard)
    )

2. Retry Failed Payments Gracefully

MAX_RETRIES = 3

async def retry_payment(order_id: str, user_id: int, context: ContextTypes.DEFAULT_TYPE, attempt: int = 1):
    """Retry payment with backoff."""
    if attempt > MAX_RETRIES:
        await context.bot.send_message(
            chat_id=user_id,
            text="❌ Payment failed after retries. Request a new link: /buy"
        )
        return False

    settlement = await check_settlement(order_id, max_wait_seconds=300)

    if settlement and settlement['status'] == 'settled':
        return True
    else:
        wait_seconds = 2 ** attempt  # Exponential backoff: 2s, 4s, 8s
        await asyncio.sleep(wait_seconds)
        return await retry_payment(order_id, user_id, context, attempt + 1)

3. Use MCP for AI-Driven Payment Workflows

If you're using Claude or another AI agent as your Telegram bot brains:

# Claude can now call:
# tools = [
#   {"name": "create_payment_request", "params": [...] },
#   {"name": "await_settlement", "params": [...] },
#   {"name": "list_orders", "params": [...] }
# ]

# Example: Claude-driven conversation
# User: "I want to buy the web dev course"
# Claude: Calls create_payment_request() → extracts checkout_url → tells user to pay
# User pays
# Claude: Calls await_settlement() → delivers course when confirmed

Do's and Don'ts

Do ✅ Don't ❌
Store order_id in a database for audit trails Store sensitive bank details in Telegram messages
Use unique order_id per transaction (include timestamp/user ID) Rely solely on client-side confirmation—always poll await_settlement()
Set reasonable QR expiry (15–30 min) via metadata Send payment requests without clearing old expired ones
Log all settlement events for reconciliation Assume payment succeeded without backend verification
Test with small amounts ($1–$5) first Hardcode prices—fetch from your database
Monitor bank feeds daily Leave polling tasks unmonitored—add error handling

FAQ

Q: Does AgentPay hold my money? No. The QR code points directly to your merchant bank account. AgentPay is infrastructure—it never touches funds.

Q: What if a user pays twice by accident? Each payment request has a unique order_id. Check your bank feed for duplicates and process refunds manually (or automate via bank API). Log duplicates and contact the user.

Q: Can I refund payments? Yes, via your bank app or API. AgentPay doesn't refund—it's your transaction. Many Vietnamese banks support automated refunds; check yours.

Q: How fast is settlement? Most Vietnamese banks settle within 1–5 minutes for same-bank transfers, 2–24 hours for cross-bank. AgentPay notifies you via bank feed as soon as it clears.

Q: Is there a fee? AgentPay itself has no transaction fee (open-source, MIT license). Your bank may charge standard VietQR transfer fees (~0–0.5% depending on the bank).

Key Takeaways

Next Steps

You now have a blueprint for monetizing on Telegram without payment complexity. Ready to build?

  1. Install: pip install agentpay-vn
  2. Get your merchant ID and API key from your Vietnamese bank (most major banks support VietQR merchant registration—free or low-cost).
  3. Check the docs: agentpay.servicesai.vn/v1/docs for full API reference.
  4. Explore the code: github.com/phuocdu/agentpay-vn — it's open-source, so fork it, extend it, and own it.
  5. Deploy: Host on Railway, Render, or your own VPS. Test with real payments.

Your Telegram bot is about to earn its first payment. That's not far away.

Get started →

← All posts