Collect VietQR Payments in Telegram Bots with AgentPay
The Problem: Why Telegram Bots Struggle With Vietnamese Payments
You've built a Telegram bot that sells digital courses, coffee subscriptions, or online services to Vietnamese customers. Sales are coming in—but payments? That's where everything breaks down.
Payment processors like Stripe have high fees (2.9% + $0.30 per transaction). International gateways impose currency conversion taxes. Bank transfer instructions are manual and error-prone. Worst of all, you're juggling multiple systems: one for the bot, another for payment processing, another for settlement confirmation.
By the time a customer clicks "Buy," you've lost them to friction.
VietNam's VietQR system solves this elegantly—it's instant, built into every smartphone banking app, and costs nearly nothing. But integrating it into a Telegram bot? That's been the missing piece. Until now.
What AgentPay Does (And Doesn't)
AgentPay VN is an open-source Python SDK + MCP server that bridges Telegram bots to VietQR payments. Here's the critical distinction: AgentPay never touches your money. It generates a QR code that points directly to your merchant bank account. When a customer scans and pays, their bank confirms settlement to your system.
This means: - Zero fraud risk on your end (you're not a payment intermediary) - Instant settlement (no waiting 3–5 business days) - Transparent fees (only your bank's standard rates apply) - MIT-licensed (you own the integration, no vendor lock-in)
The tech stack: Python SDK for payment orchestration, an MCP server for AI agent integration, and bank feed confirmation for settlement verification.
Installation and Setup
Step 1: Install the SDK
Open your terminal and run:
pip install agentpay-vn
This installs the core SDK. If you're running an MCP-enabled AI system (Claude, or custom agents), also install:
pip install agentpay-mcp
Step 2: Initialize Your Credentials
Create a .env file in your project root:
MERCHANT_BANK_ACCOUNT=0123456789
MERCHANT_BANK_NAME=Vietcombank
MERCHANT_NAME=Your Business Name
API_KEY=your_api_key_from_dashboard
Retrieve these from the AgentPay dashboard at https://agentpay.servicesai.vn/v1/docs.
Step 3: Import and Test
Create a file payment_test.py:
from agentpay_vn import AgentPayClient
import os
from dotenv import load_dotenv
load_dotenv()
client = AgentPayClient(
api_key=os.getenv("API_KEY"),
merchant_account=os.getenv("MERCHANT_BANK_ACCOUNT"),
merchant_bank=os.getenv("MERCHANT_BANK_NAME"),
merchant_name=os.getenv("MERCHANT_NAME")
)
print("AgentPay initialized successfully!")
Run it: python payment_test.py. You should see the success message.
The 3-Line Payment Flow
AgentPay simplifies payment collection into three asynchronous steps. Here's a complete, production-ready implementation:
Full Example: Course Sales Bot
import os
import asyncio
from agentpay_vn import AgentPayClient, PaymentStatus
from dotenv import load_dotenv
from telegram import Update
from telegram.ext import Application, CommandHandler, ContextTypes
load_dotenv()
client = AgentPayClient(
api_key=os.getenv("API_KEY"),
merchant_account=os.getenv("MERCHANT_BANK_ACCOUNT"),
merchant_bank=os.getenv("MERCHANT_BANK_NAME"),
merchant_name=os.getenv("MERCHANT_NAME")
)
# Step 1: Create a payment request
async def initiate_payment(user_id: str, amount: int, description: str) -> dict:
"""
Create a payment request for a customer.
Args:
user_id: Unique identifier (e.g., Telegram user ID)
amount: Price in VND (e.g., 49000 for a course)
description: What they're buying (e.g., "Python Basics Course")
Returns:
Dictionary with payment_id and checkout_url
"""
payment_request = await client.create_payment_request(
amount=amount,
description=description,
user_reference=user_id,
metadata={"course_id": "python_101", "bot_source": "telegram"}
)
return {
"payment_id": payment_request["id"],
"checkout_url": payment_request["qr_url"], # VietQR URL
"amount": amount
}
# Step 2: Send checkout link to customer
async def send_checkout(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""
Telegram /buy command: initiate payment and send QR to user.
"""
user_id = str(update.effective_user.id)
course_price = 49000 # VND
payment = await initiate_payment(
user_id=user_id,
amount=course_price,
description="Python Basics Course - 4 weeks"
)
# Send message with checkout URL
await update.message.reply_text(
f"🎓 **Python Basics Course**\n"
f"Price: {course_price:,} VND\n\n"
f"[Click here to pay via VietQR]({payment['checkout_url']})\n\n"
f"Payment ID: `{payment['payment_id']}`\n"
f"Your access will activate immediately after payment.",
parse_mode="Markdown"
)
# Store payment_id for later verification
context.user_data["pending_payment_id"] = payment["payment_id"]
# Step 3: Await settlement confirmation
async def verify_payment(user_id: str, payment_id: str, timeout: int = 600) -> bool:
"""
Poll the bank feed for settlement confirmation.
Args:
user_id: Customer identifier
payment_id: Payment request ID from Step 1
timeout: Max seconds to wait (default 10 minutes)
Returns:
True if payment settled, False if timeout/failed
"""
elapsed = 0
poll_interval = 5 # Check every 5 seconds
while elapsed < timeout:
status = await client.await_settlement(
payment_id=payment_id,
timeout=poll_interval
)
if status["status"] == PaymentStatus.SETTLED:
print(f"✅ Payment {payment_id} confirmed by bank")
return True
elif status["status"] == PaymentStatus.FAILED:
print(f"❌ Payment {payment_id} failed or cancelled")
return False
elapsed += poll_interval
await asyncio.sleep(poll_interval)
print(f"⏱️ Payment {payment_id} timeout after {timeout}s")
return False
# Integration with Telegram message handler
async def handle_payment_confirmation(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""
Periodically check if pending payment has settled.
Run this in a background job or webhook.
"""
user_id = str(update.effective_user.id)
payment_id = context.user_data.get("pending_payment_id")
if not payment_id:
await update.message.reply_text("No pending payment found. Use /buy to start.")
return
await update.message.reply_text("🔄 Checking payment status...")
if await verify_payment(user_id, payment_id, timeout=600):
await update.message.reply_text(
"🎉 Payment confirmed!\n"
"Your Python Basics course access is now active.\n"
"Check your email for the enrollment link."
)
# Unlock content in your system
# unlock_course_access(user_id, "python_101")
else:
await update.message.reply_text(
"Payment not received yet. Please:\n"
"1. Check your bank app for the transaction\n"
"2. Wait 2–3 minutes for bank confirmation\n"
"3. Use /check again"
)
# Telegram bot setup
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
await update.message.reply_text(
"Welcome to Course Bot!\n"
"/buy - Purchase Python Basics Course (49,000 VND)\n"
"/check - Check payment status"
)
async def main():
app = Application.builder().token(os.getenv("TELEGRAM_BOT_TOKEN")).build()
app.add_handler(CommandHandler("start", start))
app.add_handler(CommandHandler("buy", send_checkout))
app.add_handler(CommandHandler("check", handle_payment_confirmation))
await app.run_polling()
if __name__ == "__main__":
asyncio.run(main())
Line-by-line explanation:
-
create_payment_request(): Generates a unique payment ID and VietQR checkout URL. The SDK returns these immediately—no external API call delays. -
send_checkout(): The/buycommand creates a payment and sends the customer a Telegram message with the VietQR link. They scan it with their banking app and complete the transfer in seconds. -
verify_payment()/await_settlement(): Polls AgentPay's bank feed confirmation. When the customer's bank confirms the transfer to your merchant account, the SDK returnsSETTLED. You then unlock their access.
MCP Server Configuration for AI Agents
If you're running Claude or another MCP-compatible AI, configure it to use AgentPay tools:
{
"mcpServers": {
"agentpay": {
"command": "python",
"args": ["-m", "agentpay_mcp"],
"env": {
"AGENTPAY_API_KEY": "your_api_key",
"MERCHANT_ACCOUNT": "0123456789",
"MERCHANT_BANK": "Vietcombank",
"MERCHANT_NAME": "Your Business"
}
}
}
}
Save this as .claude.json (if using Claude Desktop). The AI can now invoke create_payment_request and await_settlement as native tools—perfect for automating payment flows in conversational UIs.
Real-World Scenario: Café Pre-Order Bot
Let's walk through a concrete use case.
The Setup: An espresso café in Ho Chi Minh City runs a Telegram bot where customers pre-order coffee for pickup. Current problem: they manually request bank transfers, which is chaotic and error-prone.
With AgentPay:
- Customer messages
/preorder→ bot sends menu - Customer selects "2× Cappuccino + 1× Iced Tea" (125,000 VND)
- Bot calls
create_payment_request()and sends VietQR checkout link - Customer scans QR in their bank app, completes transfer in 10 seconds
- Bot's
await_settlement()detects the payment (usually within 30 seconds) - Order automatically printed in the café kitchen
- Customer gets a notification: "Ready for pickup in 10 minutes!"
The numbers: - Traditional bank transfer: 5–10 minutes of manual back-and-forth - AgentPay flow: 30–60 seconds end-to-end - Cost: ~0% (vs. 2–3% with Stripe) - Customer experience: 100% frictionless
Do's and Don'ts
| ✅ Do | ❌ Don't |
|---|---|
Store payment_id in your database for audit trails |
Try to "reverse" or "refund" payments—use your bank's tools |
Set timeout=600 (10 min) for settlement polling |
Wait indefinitely; always have a timeout to prevent bot hangs |
Include meaningful metadata (course_id, user_id) for tracking |
Expose your API_KEY in client-side code; keep it server-side only |
| Test with small amounts (1,000–5,000 VND) first | Assume await_settlement() is instant; add 5–10 second polling intervals |
Use /v1/docs to verify your merchant account is active |
Store customer bank details—AgentPay never asks for them |
Advanced Tips
Idempotency: Prevent Double Charges
Network hiccups can cause duplicate requests. Use idempotency keys:
import uuid
idempotency_key = str(uuid.uuid4())
payment = await client.create_payment_request(
amount=49000,
description="Course",
user_reference=user_id,
idempotency_key=idempotency_key # Same key = same payment_id
)
Retry Logic for Settlement Polling
Network glitches can interrupt polling. Add exponential backoff:
import random
async def verify_payment_with_retry(payment_id: str, max_retries: int = 5):
for attempt in range(max_retries):
try:
status = await client.await_settlement(payment_id, timeout=10)
return status["status"] == PaymentStatus.SETTLED
except Exception as e:
wait_time = 2 ** attempt + random.uniform(0, 1)
print(f"Retry {attempt + 1}/{max_retries} after {wait_time:.1f}s")
await asyncio.sleep(wait_time)
return False
Webhook Alternative (Optional)
Instead of polling, AgentPay can POST to your webhook when payment settles:
payment = await client.create_payment_request(
amount=49000,
description="Course",
user_reference=user_id,
webhook_url="https://yourbot.com/payment-webhook" # Receives POST when settled
)
FAQ
Q: What happens if a customer doesn't pay within 10 minutes? A: The payment request expires and a new one must be created. The old QR code becomes inactive. This is by design to prevent accidental old-payment rescans.
Q: Can I refund a payment? A: AgentPay doesn't handle refunds—you do, via your bank app. The customer's money goes directly to your account. This is a feature, not a bug: you're in control, not a third party.
Q: Does AgentPay work with international customers? A: Only for customers with Vietnamese VietQR-enabled bank accounts. The QR standard doesn't support cross-border transfers yet.
Q: What if the bot goes offline during settlement polling? A: Use webhooks (above) instead of polling. Or store pending payments in a database and resume polling when the bot restarts.
Key Takeaways
- VietQR + AgentPay = instant, zero-fee payments in Telegram bots
- AgentPay never holds money; QR points directly to your merchant account
- 3-step flow:
create_payment_request()→ send URL →await_settlement() - Install in seconds:
pip install agentpay-vn - Bank-confirmed: Settlement is real-time and irreversible
- MIT licensed: Own your integration, no vendor lock-in
- MCP-compatible: AI agents can orchestrate payments natively
Get Started Now
- Install the SDK:
pip install agentpay-vn - Read the full docs: https://agentpay.servicesai.vn/v1/docs
- Explore the source: https://github.com/phuocdu/agentpay-vn
- Build your bot: Use the course-sales example above as a template
Your Telegram bot deserves effortless payments. AgentPay delivers exactly that.