Collect VietQR Payments in Telegram Bots with AgentPay
The Problem: Why Most Telegram Payment Bots Fail
You've built a Telegram bot that sells digital courses, takes coffee orders, or offers freelance services. Traffic is flowing. But when your bot tries to collect payment, you hit a wall:
- Payment gateways demand hefty setup fees and compliance audits
- You end up holding customer money in an intermediary account (legal liability)
- Settlement takes 3–5 days, frustrating both you and buyers
- Integration is a maze of webhooks, IPNs, and error handling
A founder we know spent two weeks wrestling with Stripe's Telegram integration, only to discover the monthly fee ate 40% of his margins on $2–5 transactions. He needed something lighter, faster, and truly peer-to-peer.
Enter AgentPay VN: an open-source Python SDK that lets your Telegram bot generate QR codes pointing directly to your bank account. No middleman. No holding funds. Just instant VietQR payments via the banking system Vietnameses already use daily.
What Is AgentPay VN?
AgentPay VN is an MIT-licensed Python SDK + MCP server that orchestrates VietQR payment requests. Here's the mental model:
- You create a payment request in your bot (e.g., "Customer ordered coffee for 50,000 VND")
- AgentPay generates a checkout URL with an embedded QR code
- Customer scans → pays → bank confirms settlement in seconds
- Your bot receives a webhook callback and fulfills the order
The key: AgentPay never touches money. The QR points straight at your merchant bank account. A bank feed confirms settlement. You own the transaction end-to-end.
Why Telegram + VietQR?
Telegram has 180+ million users, with especially strong adoption in Southeast Asia. Vietnamese merchants already use banking apps (MB Bank, Techcombank, VCB) that natively support VietQR scanning. Your bot becomes a natural extension of their daily workflow:
- Customer receives a payment link in chat
- Opens the QR code (in-app or screenshot)
- Scans with their banking app (2–3 taps)
- Money clears to your account in minutes
- Bot auto-confirms and ships the digital good / service
No authentication screens. No signup. Pure banking rails.
Step 1: Installation & Setup
Install AgentPay SDK
pip install agentpay-vn
Verify the installation:
python -c "import agentpay; print(agentpay.__version__)"
Get Your Merchant Credentials
You'll need: - Merchant ID (from your bank's VietQR partner) - API Key (provided by the bank or AgentPay partner) - Bank Account details (IBAN/ACN)
For testing, AgentPay provides a sandbox environment. Check the official docs for your bank.
Set Environment Variables
export AGENTPAY_MERCHANT_ID="your_merchant_id"
export AGENTPAY_API_KEY="your_api_key"
export TELEGRAM_BOT_TOKEN="your_telegram_token"
Step 2: Build Your First Payment Bot
Here's a minimal Telegram bot that collects a payment:
import os
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
from telegram.ext import Application, CommandHandler, ContextTypes
from agentpay import AgentPayClient
# Initialize AgentPay
agentpay_client = AgentPayClient(
merchant_id=os.getenv("AGENTPAY_MERCHANT_ID"),
api_key=os.getenv("AGENTPAY_API_KEY"),
)
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Send welcome message with a /buy command."""
await update.message.reply_text(
"🎉 Welcome! Use /buy to purchase an item.\n"
"Type /help for more info."
)
async def buy(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Create a payment request and send checkout link."""
user_id = update.effective_user.id
# Step 1: Create a payment request
# This tells AgentPay: "I want 50,000 VND from user X"
payment_request = agentpay_client.create_payment_request(
amount=50000, # VND
currency="VND",
merchant_ref_id=f"order_{user_id}_{int(time.time())}",
description="Premium Course - Python Mastery",
metadata={"user_id": user_id, "product": "course_python"},
)
# Step 2: Get the checkout URL with embedded QR
checkout_url = payment_request.get("checkout_url")
payment_id = payment_request.get("payment_id")
# Step 3: Send to user with inline button
keyboard = InlineKeyboardMarkup([
[InlineKeyboardButton("💳 Pay with VietQR", url=checkout_url)]
])
await update.message.reply_text(
f"📦 Order Summary:\n"
f"Product: Premium Course\n"
f"Price: 50,000 VND\n\n"
f"Tap the button below to pay with your banking app.",
reply_markup=keyboard
)
# Store payment_id for later confirmation
context.user_data["pending_payment_id"] = payment_id
async def check_payment(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Poll for payment settlement (in production, use webhooks)."""
payment_id = context.user_data.get("pending_payment_id")
if not payment_id:
await update.message.reply_text("No pending payment found.")
return
# Step 3: Await settlement confirmation
# In production, listen to bank webhooks instead of polling
status = agentpay_client.await_settlement(
payment_id=payment_id,
timeout=300, # Wait max 5 minutes
)
if status["settled"]:
await update.message.reply_text(
f"✅ Payment confirmed!\n"
f"Amount: {status['amount']:,} VND\n"
f"Your premium course access is now active.\n"
f"📚 [Open Course](https://example.com/course)"
)
# Grant access to course, send API key, etc.
else:
await update.message.reply_text(
"⏳ Payment not yet confirmed. Try again in a moment."
)
def main():
"""Start the bot."""
app = Application.builder().token(
os.getenv("TELEGRAM_BOT_TOKEN")
).build()
app.add_handler(CommandHandler("start", start))
app.add_handler(CommandHandler("buy", buy))
app.add_handler(CommandHandler("check", check_payment))
print("🤖 Bot running...")
app.run_polling()
if __name__ == "__main__":
import time
main()
Line-by-line breakdown:
- Lines 10–12: Initialize AgentPayClient with credentials from environment
- Lines 19–21:
/startsends a welcome message - Lines 23–37:
/buycreates a payment request with amount, reference ID, and metadata - Lines 39–41: Extract the checkout URL (contains QR) and payment ID
- Lines 43–47: Send an inline button linking to the checkout URL
- Lines 51–65:
/checkpolls for settlement status (use webhooks in production!) - Lines 67–74: On settlement, confirm and grant access
Step 3: Production Setup with Webhooks
Polling is fine for demos, but slow and unreliable. Use bank webhooks to get instant payment confirmation:
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route("/webhook/agentpay", methods=["POST"])
def handle_payment_webhook():
"""AgentPay calls this when payment settles."""
payload = request.json
# Verify the signature (AgentPay docs explain this)
if not agentpay_client.verify_webhook_signature(
payload, request.headers.get("X-Signature")
):
return {"error": "Invalid signature"}, 401
payment_id = payload["payment_id"]
status = payload["status"] # "settled" or "failed"
amount = payload["amount"]
merchant_ref_id = payload["merchant_ref_id"]
if status == "settled":
# Extract user ID from merchant_ref_id
user_id = int(merchant_ref_id.split("_")[1])
# Grant access to the user
# e.g., send them an API key, activate subscription, etc.
grant_access(user_id)
# Notify user via Telegram
asyncio.run(
send_telegram_message(
user_id,
f"✅ Payment received! ({amount:,} VND)\n"
"Your access is active."
)
)
return {"status": "ok"}, 200
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000)
Configure your webhook URL in the AgentPay dashboard to https://yourdomain.com/webhook/agentpay.
Step 4: Use AgentPay with Claude (MCP Server)
If you're using Claude as your AI agent orchestrator, install the MCP server:
pip install agentpay-mcp
Configure Claude's MCP settings:
{
"mcpServers": {
"agentpay": {
"command": "agentpay-mcp",
"env": {
"AGENTPAY_MERCHANT_ID": "your_merchant_id",
"AGENTPAY_API_KEY": "your_api_key"
}
}
}
}
Now Claude can call AgentPay directly:
User: "I want to sell a course for 200,000 VND."
Claude: I'll create a payment request for you.
[Calls agentpay.create_payment_request()]
Claude: Payment link: https://checkout.agentpay.vn/...
Real-World Example: Coffee Shop Telegram Bot
Imagine a café selling espresso shots via Telegram:
- Customer:
/order→ "1x Espresso (40,000 VND)" - Bot: Creates payment request, sends QR code
- Customer: Scans QR, confirms in banking app (10 seconds)
- Bank: Settles to café's account instantly
- Webhook: Fires, bot receives settlement notification
- Bot: "✅ Order confirmed! Your coffee is ready at counter #3"
- Café owner: Checks dashboard, sees +40,000 VND received
The entire flow takes 30 seconds. No payment processor fees. No settlement delays.
Advanced Tips & Gotchas
Tip 1: Use Idempotent Keys
Always include a unique merchant_ref_id to prevent duplicate charges if the network retries:
import uuid
merchant_ref_id = f"order_{user_id}_{uuid.uuid4()}"
Tip 2: Handle Webhook Timeouts
Not all webhooks arrive instantly. Implement retry logic:
from tenacity import retry, stop_after_attempt, wait_exponential
@retry(stop=stop_after_attempt(5), wait=wait_exponential())
async def notify_user_via_telegram(user_id, message):
# Attempt up to 5 times with exponential backoff
await bot.send_message(user_id, message)
Tip 3: Store Payment State
Use a database (PostgreSQL, MongoDB) to track payment status:
class PaymentRecord(db.Model):
payment_id = db.Column(db.String, primary_key=True)
user_id = db.Column(db.Integer)
amount = db.Column(db.Integer) # VND
status = db.Column(db.String) # pending, settled, failed
created_at = db.Column(db.DateTime, default=datetime.now)
Tip 4: Test on Sandbox First
AgentPay provides a sandbox environment. Always test before going live:
agentpay_client = AgentPayClient(
merchant_id="sandbox_merchant_id",
api_key="sandbox_api_key",
environment="sandbox", # Switch to "production" later
)
Do's and Don'ts
| ✅ Do | ❌ Don't |
|---|---|
| Use webhook callbacks for payment confirmation | Poll the API in a tight loop |
| Store merchant_ref_id; match it in webhook | Trust the order of webhook events |
| Verify webhook signatures | Skip signature verification |
| Implement idempotent request IDs | Assume network calls always succeed |
| Start with sandbox environment | Go live without testing |
| Monitor settlement times | Assume instant settlement (usually seconds, but be safe) |
| Log all payment events | Ignore payment failures silently |
FAQ
Q: Can AgentPay handle payments in other currencies (USD, etc.)?
A: Currently, AgentPay VN is designed for VND (Vietnamese Dong) via VietQR, which is a Vietnamese banking standard. International currency support would require additional bank partnerships.
Q: What if the customer's bank doesn't support VietQR?
A: Most Vietnamese banks (MB, Vietcombank, Techcombank, ACB, VP Bank, etc.) support VietQR. If a customer's bank doesn't, they can ask their bank to enable it, or you can provide a fallback (e.g., direct bank transfer details).
Q: How much does AgentPay cost?
A: AgentPay itself is free (MIT open-source). You pay your bank's standard VietQR transaction fees (typically 0–0.5%), which is much cheaper than Stripe (2.9% + $0.30) or PayPal (3.5%).
Q: How long does settlement take?
A: Usually 1–5 seconds. The money goes directly to your bank account, so it's as fast as an interbank transfer. Some banks may batch settlements end-of-day, but that's rare.
Key Takeaways
- No middleman: AgentPay never holds funds; QR codes point straight to your bank account
- Fast setup: Install with
pip install agentpay-vnand get your first payment in under 10 minutes - Telegram-native: Customers pay without leaving Telegram; no signup required
- Low fees: Pay only your bank's VietQR fees (0–0.5%), not 3% payment processor cuts
- Production-ready: Use webhooks, idempotent IDs, and proper error handling
- Open-source: MIT license means full transparency and community support
- MCP integration: Use with Claude or other AI agents for autonomous payment orchestration
Next Steps
- Install AgentPay:
pip install agentpay-vn - Get credentials: Contact your bank or visit agentpay.servicesai.vn
- Clone the repo: github.com/phuocdu/agentpay-vn
- Read the docs: agentpay.servicesai.vn/v1/docs
- Build your bot: Follow the examples above and ship!
Your Telegram payment bot is minutes away. No more payment processor headaches. Just you, your customers, and the Vietnamese banking system doing what it does best.