DEV Community

ServicesAI VN
ServicesAI VN

Posted on • Originally published at agentpay.servicesai.vn

Collect VietQR Payments in Telegram Bots with AgentPay

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
Enter fullscreen mode Exit fullscreen mode

Verify it works:

python -c "import agentpay; print(agentpay.__version__)"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

Line-by-line explanation:

  • Lines 10-14: Initialize AgentPayClient with your credentials.
  • Lines 23-39: create_payment_request() generates a unique payment link tied to this user's order.
  • Lines 42-44: Store the order locally so we can track who paid for what.
  • Lines 46-54: Build a Telegram inline button linking to the checkout URL (which displays the VietQR code).
  • Lines 57: Kick off a background task to wait for settlement—the user doesn't have to stay in the chat.
  • Lines 62-86: await_settlement() polls the bank feed until payment is confirmed. Once confirmed, we grant course access.
  • Lines 88-105: Optional /check_payment command lets users manually check if payment went through.

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
Enter fullscreen mode Exit fullscreen mode

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"
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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."
Enter fullscreen mode Exit fullscreen mode

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}"
)
Enter fullscreen mode Exit fullscreen mode

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, ...)
Enter fullscreen mode Exit fullscreen mode

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!")
Enter fullscreen mode Exit fullscreen mode

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

  • VietQR + AgentPay = friction-free payments in Telegram bots without holding customer funds.
  • Installation is one line: pip install agentpay-vn
  • The flow is simple: create_payment_request()send checkout_url()await_settlement()
  • Bank confirmation is real: Your bank feed confirms every transaction—no fake payments.
  • Works at scale: Use webhooks for thousands of concurrent users; use polling for hobby bots.
  • MIT licensed & open-source: Full transparency, no vendor lock-in.
  • Vietnam-specific: Built for VietQR, integrated with major Vietnamese banks.

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
Enter fullscreen mode Exit fullscreen mode

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! 🎉

Top comments (0)