Collect VietQR Payments in Telegram Bots with AgentPay

2026-07-06 · AgentPay VN

telegrampaymentvietqrpythonagentpay

The Problem: Getting Paid Through Telegram Is Messy

You've built a useful Telegram bot—maybe it sells online courses, takes café orders, or books freelance services. Your users love it. But when payment time comes, everything breaks down. You're manually sending payment screenshots, users are using unsafe third-party links, and nobody's sure if money actually arrived. You definitely don't want to hold customer funds in your own account (legal liability, licensing headaches). What you need is a way for your bot to generate a VietQR code pointing directly at your bank account, and then automatically confirm when the payment settles. That's where AgentPay VN comes in.

Why VietQR + AgentPay Is the Right Move

VietQR is Vietnam's national instant payment standard—every bank supports it, every Vietnamese smartphone has a QR scanner built-in, and users trust it because it uses their actual bank app. AgentPay VN is an MIT-licensed Python SDK that wraps the complexity away. It never touches your money (the QR points straight to your bank), and a real bank feed confirms settlement in seconds. No middleman fees, no foreign exchange hassles, no merchant account approval delays.

The magic is in three lines: 1. create_payment_request — generate a unique request tied to your bot order 2. send checkout_url — post the QR code to your user 3. await_settlement — block until the bank confirms payment

That's it. Let's build it.

Installation and Setup

Step 1: Install the SDK

pip install agentpay-vn

Verify it works:

python -c "import agentpay; print(agentpay.__version__)"

Step 2: Set Up Your Bank Feed (One-Time)

AgentPay needs a way to know when your bank receives money. You'll configure a webhook or use AgentPay's bank integration partner. Visit https://agentpay.servicesai.vn/v1/docs for your bank's specific setup—most Vietnamese banks (Vietcombank, Techcombank, Agribank, etc.) are already supported.

Step 3: Get Your API Credentials

Sign up at https://agentpay.servicesai.vn and grab: - AGENTPAY_API_KEY — your secret key - AGENTPAY_MERCHANT_ID — identifies your account - TELEGRAM_BOT_TOKEN — from BotFather

Store these in a .env file (never commit it):

AGENTPAY_API_KEY=your_key_here
AGENTPAY_MERCHANT_ID=your_merchant_id
TELEGRAM_BOT_TOKEN=your_bot_token

Building Your First Payment Flow

Full Working Example: Course Sales Bot

Let's say you're selling a "Python Mastery" course for 299,000 VND via Telegram. Here's the complete flow:

import asyncio
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
from telegram.ext import Application, CommandHandler, ContextTypes
from agentpay import AgentPayClient, PaymentRequest
import os
from dotenv import load_dotenv

load_dotenv()

# Initialize AgentPay
client = AgentPayClient(
    api_key=os.getenv("AGENTPAY_API_KEY"),
    merchant_id=os.getenv("AGENTPAY_MERCHANT_ID")
)

app = Application.builder().token(os.getenv("TELEGRAM_BOT_TOKEN")).build()

# Store pending payments in memory (use Redis in production)
pending_payments = {}

async def start_course_purchase(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """
    User sends /buy_course — we create a payment request
    """
    user_id = update.effective_user.id
    course_price = 299000  # VND
    course_name = "Python Mastery 2024"

    # Step 1: Create a payment request with AgentPay
    payment_request = await client.create_payment_request(
        amount=course_price,
        description=f"Course: {course_name}",
        order_id=f"course_{user_id}_{int(asyncio.get_event_loop().time())}",
        customer_name=update.effective_user.full_name or "Anonymous",
        customer_phone="",  # Optional; collect if needed
        redirect_url="https://t.me/your_bot_username"  # Where to send user after payment
    )

    # Step 2: Store the payment request locally (for later confirmation)
    pending_payments[payment_request.order_id] = {
        "user_id": user_id,
        "amount": course_price,
        "course": course_name
    }

    # Step 3: Send checkout URL to user with QR code
    checkout_url = payment_request.checkout_url
    keyboard = InlineKeyboardMarkup(
        [[InlineKeyboardButton("💳 Pay with VietQR", url=checkout_url)]]
    )

    await update.message.reply_text(
        f"🎓 Ready to buy **{course_name}**?\n\n"
        f"Price: **{course_price:,} VND**\n\n"
        f"Click the button below to scan the QR code with your bank app. "
        f"You'll be redirected back here once payment is confirmed.",
        reply_markup=keyboard,
        parse_mode="Markdown"
    )

    # Step 4 (Background): Wait for settlement
    asyncio.create_task(wait_for_payment(payment_request.order_id, user_id, course_name))

async def wait_for_payment(order_id: str, user_id: int, course_name: str):
    """
    Poll AgentPay until payment settles (timeout: 30 minutes)
    """
    max_wait = 1800  # 30 minutes
    elapsed = 0

    while elapsed < max_wait:
        try:
            # Check if payment has settled
            settlement = await client.await_settlement(
                order_id=order_id,
                timeout=10  # Check every 10 seconds
            )

            if settlement.status == "confirmed":
                # Payment successful! Grant access to course
                await app.bot.send_message(
                    chat_id=user_id,
                    text=f"✅ **Payment confirmed!**\n\n"
                         f"Your access to {course_name} is now active.\n"
                         f"Check your email for login details.\n\n"
                         f"Transaction ID: `{settlement.transaction_id}`",
                    parse_mode="Markdown"
                )

                # Log the successful transaction
                print(f"Payment confirmed: {order_id} | Amount: {settlement.amount} VND")
                return

        except asyncio.TimeoutError:
            elapsed += 10
        except Exception as e:
            print(f"Error checking payment {order_id}: {e}")
            await asyncio.sleep(10)
            elapsed += 10

    # Timeout — notify user
    await app.bot.send_message(
        chat_id=user_id,
        text=f"⏱️ Payment timeout. If you already paid, please wait a few seconds and try /check_payment\n"
             f"If not, use /buy_course to try again."
    )

async def check_payment(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """
    User can manually check status with /check_payment
    """
    user_id = update.effective_user.id
    user_orders = [o for o, p in pending_payments.items() if p["user_id"] == user_id]

    if not user_orders:
        await update.message.reply_text("No pending payments found.")
        return

    for order_id in user_orders:
        try:
            settlement = await client.get_payment_status(order_id)
            if settlement.status == "confirmed":
                await update.message.reply_text(f"✅ Payment {order_id} is confirmed!")
            else:
                await update.message.reply_text(f"⏳ Payment {order_id} is still pending...")
        except Exception as e:
            await update.message.reply_text(f"Error checking status: {e}")

app.add_handler(CommandHandler("buy_course", start_course_purchase))
app.add_handler(CommandHandler("check_payment", check_payment))

if __name__ == "__main__":
    app.run_polling()

Line-by-line explanation:

Using AgentPay with Claude and MCP

If you're using AI agents (Claude, etc.) to manage your bot, AgentPay provides an MCP (Model Context Protocol) server. Install it:

pip install agentpay-mcp

Configure it in your Claude settings (or .mcp.json):

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

Now Claude can call AgentPay functions directly:

User: "A customer wants to buy my e-book for 99,000 VND. Create a payment link."

Claude: I'll use AgentPay to create a payment request for you.
[Calls: create_payment_request(amount=99000, description="E-book", ...)]
→ Returns checkout_url

Claude: "Here's your payment link: [link]. Share it with your customer."

Real-World Walkthrough: Café Ordering Bot

Imagine a small coffee shop in Saigon. They built a Telegram bot where customers can order coffee, pay, and pick up. Here's how AgentPay fits:

  1. Order placed: Customer types /order → selects "2x Cà Phê Đen + 1x Trà Lạnh" (120,000 VND).
  2. Payment link sent: Bot calls create_payment_request() → generates VietQR code → sends link to customer.
  3. Customer scans: User opens their Agribank/Vietcombank app → scans QR → confirms transfer.
  4. Settlement confirmed: Bank webhook hits AgentPay → await_settlement() resolves → bot sends "Order ready! Pick up at counter #3."
  5. Record kept: Shop owner can see all payments in their AgentPay dashboard (linked to their actual bank statement).

Total time: 2 minutes. No middle app, no fees beyond normal bank transfer, no fraud risk.

Do's and Don'ts

✅ Do ❌ Don't
Use .env files for secrets Hardcode API keys in source code
Implement retry logic on timeouts Assume QR codes are unique per user (they're not—reuse is safe)
Log transaction IDs for reconciliation Hold customer funds in a separate account
Set reasonable timeouts (5–30 min) Use old/revoked QR codes
Test with small amounts first Ignore bank feed errors silently
Store order metadata (product, user, timestamp) Forget to handle network disconnects

Advanced Tips

Tip 1: Batch Payments

If you're selling multiple items, don't create separate payment requests. Sum the total and create one request:

total = sum([item["price"] for item in cart])
await client.create_payment_request(
    amount=total,
    description=f"{len(cart)} items",
    order_id=f"cart_{user_id}_{timestamp}"
)

Tip 2: Idempotency

If your bot crashes mid-payment, reuse the same order_id to avoid double-charging:

order_id = f"course_{user_id}_{course_id}"  # Deterministic
settlement = await client.get_payment_status(order_id)
if settlement and settlement.status == "confirmed":
    # Already paid—grant access
    pass
else:
    # Create new request
    await client.create_payment_request(order_id=order_id, ...)

Tip 3: Webhooks Instead of Polling

For production bots with thousands of users, polling is inefficient. Configure a webhook with AgentPay:

@app.post("/agentpay/webhook")
async def handle_settlement(payload: dict):
    order_id = payload["order_id"]
    status = payload["status"]

    if status == "confirmed":
        user_id = pending_payments[order_id]["user_id"]
        await app.bot.send_message(user_id, "✅ Payment confirmed!")

This way, AgentPay notifies you when payment settles—no polling required.

FAQ

Q: Does AgentPay hold my money? No. The VietQR code points directly to your bank account. AgentPay only confirms settlement via your bank feed—it never touches the funds.

Q: What if a customer disputes the payment? AgentPay logs all transactions with bank-confirmed transaction IDs. Disputes go through your bank's normal chargeback process—AgentPay provides the proof.

Q: Can I use AgentPay outside Vietnam? Currently, VietQR is Vietnam-only. If you want to accept international payments, you'll need a separate solution (Stripe, PayPal).

Q: How long does settlement confirmation take? Typically 2–10 seconds after the customer completes the transfer. Occasionally up to 2 minutes depending on the bank.

Q: Do I need to register my bot with the government? Regulations vary. For educational or small commercial bots, you're usually fine. For large-scale commerce, consult a Vietnamese lawyer about merchant licensing.

Key Takeaways

Get Started Now

You have everything you need. Install AgentPay VN, wire up a test bot, and accept your first payment in under 20 minutes.

pip install agentpay-vn

Read the full API reference and bank-specific setup guides at https://agentpay.servicesai.vn/v1/docs and explore the source code on GitHub.

Have questions? Join the community, open an issue, or reach out. Happy collecting! 🎉

Get started →

← All posts