Agent-Native Payments: Idempotency, Webhooks & Settlement
The Problem: Why Your Agent's Payment Logic Is Breaking
Imagine your AI agent is selling online courses. A customer asks to enroll in a Python masterclass. The agent:
- Creates a payment request
- Sends the checkout URL
- Waits for settlement confirmation
Then the customer's network hiccups. The webhook arrives twice. Your agent processes payment twice. Or worse—your agent retries the payment request, and now you have three identical orders in the database with three separate QR codes pointing to the merchant's account.
This is the core challenge of agent-native payments: unlike human-controlled checkout flows where a browser prevents double-clicks, an AI agent can retry indefinitely. Without idempotency, webhooks become a liability rather than a feature.
AgentPay VN solves this by baking idempotency into the SDK and making settlement confirmation rock-solid. Let's build the right way.
What Makes AgentPay VN Different
AgentPay VN is an open-source (MIT) Python SDK + MCP server that lets AI agents collect VietQR payments without ever touching the money. Here's the architecture:
- No custody: The QR code points directly to the merchant's bank account
- Bank-confirmed settlement: A real bank feed confirms money arrived
- Idempotent by design: Same request = same payment ID, no duplicates
- Agent-ready: Works seamlessly with Claude via the MCP server
Install is one line: pip install agentpay-vn
The payment flow is dead simple:
create_payment_request() → send checkout_url → await_settlement()
But the devil is in how you use it. Let's dig into the patterns that prevent payment chaos.
Understanding Idempotency in Agent Payments
Why Idempotency Matters for Agents
Idempotency means: calling the same function multiple times with the same inputs produces the same result, only once.
In the AgentPay VN SDK, when you create a payment request with idempotency in mind:
from agentpay_vn import create_payment_request
# Agent processes order #12345
order_id = "order_12345"
amount = 299000 # VND
description = "Python Masterclass Enrollment"
# First call: creates payment
payment = create_payment_request(
amount=amount,
description=description,
idempotency_key=f"order_{order_id}" # <-- This is critical
)
print(f"Payment ID: {payment['id']}") # e.g., "pay_abc123"
print(f"QR URL: {payment['checkout_url']}")
# Agent's network glitches; it retries the same call
payment_retry = create_payment_request(
amount=amount,
description=description,
idempotency_key=f"order_{order_id}" # Same key!
)
# Result: SAME payment ID returned. No duplicate.
assert payment['id'] == payment_retry['id']
The idempotency_key is your safety net. AgentPay VN (backed by the payment provider) guarantees:
- Same key → same payment object, no matter how many times you call
- Different key → new payment object
Best practice: Derive the idempotency key from something immutable in your order, never from timestamps or random UUIDs.
How to Structure Your Agent Logic
Here's a robust pattern:
from agentpay_vn import create_payment_request, await_settlement
import hashlib
import json
class PaymentAgent:
def __init__(self, api_key: str):
self.api_key = api_key
def process_order(self, order_data: dict) -> dict:
"""
Process a payment for an order, idempotently.
order_data = {"user_id": "user_42", "product_id": "course_99", "amount": 299000}
"""
# Step 1: Generate idempotency key from order content (not timestamp)
order_json = json.dumps(order_data, sort_keys=True)
idempotency_key = hashlib.md5(order_json.encode()).hexdigest()
# Step 2: Create payment (idempotently)
payment = create_payment_request(
amount=order_data['amount'],
description=f"Course {order_data['product_id']} for user {order_data['user_id']}",
idempotency_key=idempotency_key,
api_key=self.api_key
)
print(f"✓ Payment created: {payment['id']}")
print(f"✓ Send customer: {payment['checkout_url']}")
# Step 3: Await settlement (with timeout)
try:
settlement = await_settlement(
payment_id=payment['id'],
timeout_seconds=300, # 5 minutes
api_key=self.api_key
)
print(f"✓ Settlement confirmed: {settlement['amount']} VND received")
return {"status": "success", "payment_id": payment['id'], "settlement": settlement}
except Exception as e:
print(f"✗ Settlement failed: {e}")
return {"status": "pending", "payment_id": payment['id']}
# Usage
agent = PaymentAgent(api_key="your_api_key")
result = agent.process_order({
"user_id": "user_42",
"product_id": "course_99",
"amount": 299000
})
Key points:
- idempotency_key is deterministic (hash of order data)
- If the agent retries, it gets the same payment ID back
- No duplicate charges
Webhooks: Real-Time Settlement Notification
webhooks are how AgentPay VN tells your agent (or backend) that money actually arrived. Unlike polling with await_settlement(), webhooks are instant and reduce latency.
Setting Up Webhooks
-
Configure your webhook endpoint in the AgentPay VN dashboard: - URL:
https://your-agent-backend.com/webhooks/agentpay- Events:payment.settled,payment.failed -
Implement the webhook handler:
from fastapi import FastAPI, Request
import hmac
import hashlib
app = FastAPI()
WEBHOOK_SECRET = "your_webhook_secret_from_dashboard"
@app.post("/webhooks/agentpay")
async def handle_settlement_webhook(request: Request):
"""
AgentPay VN calls this when payment settles.
Payload example:
{
"event": "payment.settled",
"payment_id": "pay_abc123",
"amount": 299000,
"timestamp": 1699564800,
"signature": "hmac_sha256_signature"
}
"""
payload = await request.json()
# Verify webhook signature (prevent spoofing)
signature = request.headers.get("X-AgentPay-Signature")
expected_sig = hmac.new(
WEBHOOK_SECRET.encode(),
body.encode(),
hashlib.sha256
).hexdigest()
if not hmac.compare_digest(signature, expected_sig):
return {"status": "invalid_signature"}, 401
# Handle settlement
if payload['event'] == 'payment.settled':
payment_id = payload['payment_id']
amount = payload['amount']
# Your agent logic: unlock course, send email, etc.
print(f"✓ Payment {payment_id} settled for {amount} VND")
unlock_course(payment_id)
send_confirmation_email(payment_id)
return {"status": "processed"}
return {"status": "ignored"}
Webhook Reliability: Idempotent Handlers
AgentPay VN may retry a webhook if your endpoint times out. So your handler must be idempotent: processing the same webhook twice should be safe.
# DON'T: Increment a counter
def handle_webhook_bad(payment_id):
order = get_order(payment_id)
order.credits_received += 1 # ❌ Called twice = +2 credits
order.save()
# DO: Check state first
def handle_webhook_good(payment_id):
order = get_order(payment_id)
if order.status == "pending":
order.status = "completed"
order.credits_received = True
order.save()
else:
print(f"Order {payment_id} already processed, skipping")
Real-World Walkthrough: Online Course Bot
Let's build a Claude agent that sells courses via VietQR:
MCP Server Configuration
First, set up the MCP server in your Claude config (~/.config/claude/resources.json):
{
"resources": [
{
"type": "mcp",
"name": "agentpay-vn",
"config": {
"command": "agentpay-mcp",
"env": {
"AGENTPAY_API_KEY": "your_api_key_here",
"AGENTPAY_WEBHOOK_SECRET": "your_webhook_secret"
}
}
}
]
}
The Agent Prompt
You are a helpful course enrollment assistant. When a user wants to buy a course:
1. Use the agentpay_vn tool to create_payment_request()
2. Share the checkout_url with the user
3. Call await_settlement() or wait for webhook confirmation
4. Confirm enrollment
Courses:
- Python Masterclass: 299,000 VND
- Web Dev Bootcamp: 499,000 VND
- AI Fundamentals: 199,000 VND
Conversation Flow
User: "I want to buy the Python Masterclass."
Agent (calls MCP tool):
{
"tool": "create_payment_request",
"params": {
"amount": 299000,
"description": "Python Masterclass Enrollment",
"idempotency_key": "user_42_course_99_20240115"
}
}
Response:
{
"id": "pay_xyz789",
"amount": 299000,
"checkout_url": "https://vietqr.servicesai.vn/pay/xyz789",
"status": "pending"
}
Agent (responds to user):
"Great! I've created your payment. Please scan this QR code or visit: https://vietqr.servicesai.vn/pay/xyz789. I'll confirm your enrollment once payment settles."
User scans QR, pays 299,000 VND from their bank.
AgentPay VN backend detects settlement → calls your webhook → agent unlocks course.
Agent (async): "✓ Payment confirmed! You now have access to the Python Masterclass. Check your email for login details."
Advanced Tips: Handling Edge Cases
Do's and Don'ts
| ✅ DO | ❌ DON'T |
|---|---|
| Use deterministic idempotency keys (hash of order data) | Generate random idempotency keys per retry |
| Set a reasonable timeout (5–10 min) for settlement | Wait indefinitely; timeout may indicate customer abandoned |
| Verify webhook signatures with HMAC | Trust webhooks without verification |
| Log payment IDs for auditing | Delete payment records; you need them for disputes |
| Handle webhook delivery failures gracefully | Crash if a webhook fails; log and move on |
Store the settlement confirmation from await_settlement() |
Only store the payment request |
Common Pitfalls
- Generating new idempotency keys on retry: This defeats the purpose. Always derive from immutable order data.
- Not handling concurrent requests: If two agents process the same order, they'll both call
create_payment_request()with the same idempotency key. That's fine—they'll both get the same payment ID. But make sure your agent logic doesn't double-unlock. - Ignoring webhook retries: AgentPay VN may send the same webhook 3+ times if your endpoint is slow. Make your handlers idempotent.
FAQ
Q: Does AgentPay VN hold my money? No. The QR code points directly to your merchant bank account. AgentPay VN is a conductor, not a cashier. Settlement is confirmed via a real bank feed.
Q: What if a customer scans the QR twice? The QR is valid for one payment. Scanning twice won't charge twice. If you want to limit attempts, set a payment expiry (default is 24 hours).
Q: How long until settlement is confirmed?
VietComBank and other major banks typically confirm within 5–15 minutes. Use await_settlement() with a 10-minute timeout for most use cases.
Q: Can I customize the QR code appearance?
Yes. create_payment_request() accepts optional qr_style and merchant_name params. Check the docs for details.
Key Takeaways
- Idempotency is non-negotiable: Use deterministic idempotency keys derived from order data, not timestamps. This prevents duplicate charges when agents retry.
- Webhooks are faster than polling: Set up webhook handlers and let AgentPay VN push settlement confirmations to you in real time.
- Webhook handlers must be idempotent: Check order status before processing; the same webhook may arrive multiple times.
- Money never touches AgentPay VN: The QR points straight to your bank. You control everything.
- Log payment IDs religiously: For debugging, disputes, and auditing. Never delete them.
- Test with idempotency: Write tests that call
create_payment_request()3× with the same key and verify you get the same payment ID back.
Get Started Today
Install AgentPay VN in your project:
pip install agentpay-vn
Then start the MCP server:
agentpay-mcp
Read the full API reference and explore more examples at agentpay.servicesai.vn/v1/docs.
For questions or contributions, visit the GitHub repo: https://github.com/phuocdu/agentpay-vn
Happy selling. May your agents never double-charge. 🚀