Agent-Native Payments: Idempotency, Webhooks & Settlement
The Problem: Why Your AI Agent's Payment Flow Breaks
You've built a smart chatbot that sells online courses. A customer asks to enroll, your agent creates a payment request, and generates a QR code. But here's where it gets messy:
- The webhook confirming payment arrives twice (or not at all).
- Your agent retries the request, creating duplicate charges.
- The customer sees "Payment pending" for 10 minutes, then gives up.
- You have no way to know if the money actually landed in your bank account without logging into your bank's dashboard.
This isn't hypothetical. When AI agents handle payments, traditional payment flows break down. Agents don't think like humans—they retry, fork execution paths, and operate at scale. You need payment infrastructure built for agent behavior: idempotency, reliable webhooks, and transparent settlement.
AgentPay VN solves this with an open-source Python SDK and MCP server that lets your agent collect payments directly into your merchant bank account via VietQR. No middleman holding your money. No payment gateway markup. Just your agent, your customer, and your bank.
Understanding Idempotency in Agent Payments
Idempotency is the foundation of reliable agent payments. It means: calling the same payment request twice produces the same result, not two charges.
Here's why agents need this:
- Retry Logic: Agents automatically retry failed operations. Without idempotency, a network timeout becomes a duplicate charge.
- Parallel Execution: Modern AI frameworks spawn multiple agent threads. Both might attempt payment creation simultaneously.
- Uncertainty on Failure: When a request times out, the agent doesn't know if it succeeded or failed—so it retries.
AgentPay VN handles idempotency through request IDs. Every create_payment_request call requires an idempotency_key—a unique identifier for that transaction. If you call it twice with the same key, you get the same payment request back. No duplicate charge.
How to Implement Idempotency
The key is making your idempotency key stable and deterministic:
from agentpay_vn import PaymentClient
import hashlib
# Initialize the client
client = PaymentClient(api_key="your_api_key")
# For a course enrollment, use customer_id + course_id + timestamp
customer_id = "user_12345"
course_id = "python_basics_101"
purchase_attempt = "2025-01-15T14:30:00"
# Generate a stable idempotency key
idempotency_key = hashlib.sha256(
f"{customer_id}:{course_id}:{purchase_attempt}".encode()
).hexdigest()
# Create payment request with idempotency
payment = client.create_payment_request(
idempotency_key=idempotency_key, # This makes it safe to retry
amount=299000, # Vietnamese Dong
merchant_account="0123456789", # Your bank account
description="Python Basics Course",
customer_email="student@example.com",
order_id=f"course_{course_id}_{customer_id}"
)
print(f"Checkout URL: {payment['checkout_url']}")
print(f"Request ID: {payment['request_id']}")
Line-by-line explanation:
- idempotency_key: A SHA256 hash combining customer, course, and timestamp ensures uniqueness and stability.
- amount: Always in Vietnamese Dong (VND). 299,000 VND ≈ $12 USD.
- merchant_account: Your bank account number (AgentPay never holds this money).
- If you call this function again with the same idempotency_key, you get the exact same checkout_url and request_id—no new charge created.
Webhooks: Real-Time Settlement Confirmation
Once your customer scans the QR code and pays, the bank processes the transaction. But how does your agent know? Webhooks.
AgentPay VN sends a webhook to your server when settlement is confirmed—meaning the money cleared and landed in your merchant account. This is not a pending notification; it's a "money is here" confirmation.
Setting Up Webhook Handling
from flask import Flask, request, jsonify
from agentpay_vn import verify_webhook_signature
app = Flask(__name__)
@app.route("/webhooks/payment-settled", methods=["POST"])
def handle_payment_webhook():
"""
Webhook endpoint called when AgentPay confirms settlement.
The money is already in your bank account.
"""
payload = request.json
signature = request.headers.get("X-AgentPay-Signature")
webhook_secret = "your_webhook_secret"
# Always verify the signature to prevent spoofing
if not verify_webhook_signature(payload, signature, webhook_secret):
return jsonify({"error": "Invalid signature"}), 403
# Extract payment details
request_id = payload["request_id"]
amount = payload["amount"] # VND
status = payload["status"] # "settled", "failed", etc.
timestamp = payload["settled_at"]
if status == "settled":
# Money confirmed in your bank account
print(f"Payment settled: {amount} VND (ID: {request_id})")
# Now do your business logic:
# - Enroll student in course
# - Send confirmation email
# - Log transaction
enroll_student(request_id, amount)
send_confirmation_email(request_id)
elif status == "failed":
print(f"Payment failed: {request_id}")
# Handle refund or retry logic
# Always return 200 OK to confirm receipt
# AgentPay will retry if it gets 4xx or 5xx
return jsonify({"received": True}), 200
def enroll_student(request_id, amount):
"""Example: enroll student after payment settled."""
# Query your database to find the student by request_id
# Update enrollment status
pass
def send_confirmation_email(request_id):
"""Send access credentials to student."""
pass
if __name__ == "__main__":
app.run(port=5000)
Key points:
- verify_webhook_signature(): Essential for security. Prevents attackers from spoofing payment confirmations.
- status == "settled": This means the money is already in your bank account. No pending, no risk of reversal.
- Always return 200 OK even if your business logic fails—AgentPay will retry if you return an error.
MCP Server: Integrating Payments into Claude
If you're using Claude (or another AI agent), the MCP (Model Context Protocol) server lets your agent call payment functions directly.
Configure Claude with AgentPay MCP
Add this to your Claude configuration (typically ~/.claude/config.json or your environment):
{
"mcpServers": {
"agentpay": {
"command": "agentpay-mcp",
"env": {
"AGENTPAY_API_KEY": "your_api_key_here",
"AGENTPAY_WEBHOOK_SECRET": "your_webhook_secret"
}
}
}
}
Now Claude can call these tools directly in its reasoning:
User: "I want to buy the advanced Python course for 599,000 VND."
Claude: [reasoning] The user wants to purchase. I'll create a payment request.
Tool Call: create_payment_request
- amount: 599000
- description: "Advanced Python Course - Full Lifetime Access"
- customer_email: user@example.com
Tool Result: {"checkout_url": "vietqr://...", "request_id": "req_abc123"}
Claude: "Your course access is almost ready! Scan this QR code to complete payment: [QR image]. I'll notify you as soon as your payment settles."
The agent doesn't wait for settlement—it sends the QR, then monitors via webhooks.
Real-World Walkthrough: Online Course Chatbot
Let's build a complete example: a chatbot that sells online courses.
Scenario: Customer asks to buy a course. The bot: 1. Creates a payment request (idempotent). 2. Sends a checkout link with QR code. 3. Webhook confirms settlement → bot enrolls student. 4. Student gets instant access.
from agentpay_vn import PaymentClient, await_settlement
import hashlib
import time
client = PaymentClient(api_key="your_api_key")
def sell_course_to_customer(customer_email, customer_name, course_id, course_price):
"""
Complete payment flow for course purchase.
"""
# Step 1: Create idempotency key
idempotency_key = hashlib.sha256(
f"{customer_email}:{course_id}:{int(time.time())}".encode()
).hexdigest()
# Step 2: Create payment request
print(f"Creating payment for {customer_name}...")
payment = client.create_payment_request(
idempotency_key=idempotency_key,
amount=course_price, # in VND
merchant_account="0123456789",
description=f"Course: {course_id} - {customer_name}",
customer_email=customer_email,
order_id=f"course_{course_id}_{int(time.time())}"
)
request_id = payment["request_id"]
checkout_url = payment["checkout_url"]
print(f"✓ Payment request created (ID: {request_id})")
print(f"✓ Checkout URL: {checkout_url}")
# Step 3: Wait for settlement (with timeout)
print(f"Waiting for settlement confirmation...")
settlement = await_settlement(
request_id=request_id,
timeout_seconds=300 # Wait max 5 minutes
)
if settlement["status"] == "settled":
print(f"✓ PAYMENT SETTLED: {settlement['amount']} VND")
print(f"✓ Settlement time: {settlement['settled_at']}")
# Step 4: Enroll student in course
enroll_student(customer_email, course_id)
send_course_access_email(customer_email, customer_name, course_id)
return {"success": True, "request_id": request_id, "course_id": course_id}
else:
print(f"✗ PAYMENT FAILED: {settlement['status']}")
return {"success": False, "request_id": request_id, "reason": settlement["status"]}
# Usage
result = sell_course_to_customer(
customer_email="student@example.com",
customer_name="Nguyen Van A",
course_id="python_advanced_2025",
course_price=599000 # 599,000 VND
)
print(f"\nResult: {result}")
This flow is agent-safe: - Idempotent: calling it twice with same customer+course+time → same QR - Reliable: webhook confirms money actually landed - Transparent: you see settlement status in real-time
Do's and Don'ts for Agent Payments
| ✅ Do | ❌ Don't |
|---|---|
| Always use idempotency keys for every payment | Assume a timeout means failure (always check status) |
| Verify webhook signatures | Trust webhooks without signature verification |
| Return 200 OK from webhook handlers even on error | Return 5xx if your business logic fails (causes retries) |
Store request_id for every transaction |
Create duplicate payment requests to "retry" manually |
Use await_settlement() with a timeout |
Block indefinitely waiting for settlement |
| Log webhook events and settlement confirmations | Rely only on agent memory (logs are immutable proof) |
Advanced Tips
Retry Strategy with Exponential Backoff
If await_settlement() times out, don't immediately give up:
import time
def robust_await_settlement(request_id, max_retries=3):
"""
Retry settlement check with exponential backoff.
"""
backoff = 1 # Start at 1 second
for attempt in range(max_retries):
try:
settlement = client.await_settlement(request_id, timeout_seconds=60)
if settlement["status"] == "settled":
return settlement
except TimeoutError:
print(f"Attempt {attempt + 1} timed out, retrying...")
time.sleep(backoff)
backoff *= 2 # Exponential backoff: 1s → 2s → 4s
# After retries exhausted, return last known status
return client.get_payment_status(request_id)
Handling Concurrent Payments
If multiple agents try to pay for the same thing simultaneously:
# Each agent generates the same idempotency key
# (e.g., customer_id + course_id + purchase_date)
# First agent creates payment → all agents get same QR
# Only one charge occurs, even with parallel requests
FAQ
Q: Does AgentPay hold my money? A: No. The QR code points directly at your merchant bank account. Settlement is confirmed when the money lands in your bank account, not AgentPay's. We're open-source MIT license—inspect the code yourself at https://github.com/phuocdu/agentpay-vn.
Q: What if the customer doesn't scan the QR within 24 hours? A: Payment requests expire after 24 hours. The customer can still scan, but the transaction won't settle after expiry. You can create a new payment request (with a different idempotency key) and resend the QR.
Q: Can I integrate this with Telegram/Discord bots? A: Yes. The SDK works with any Python bot framework. Generate the QR code URL and send it to the chat. Webhook confirms settlement and you post a follow-up message to the customer.
Q: What happens if my webhook endpoint is unreachable? A: AgentPay retries the webhook 5 times over 24 hours with exponential backoff. Always expose your webhook to the internet and monitor it. For testing locally, use ngrok or similar tunneling.
Key Takeaways
- Idempotency is non-negotiable for agent payments. Always use
idempotency_keyto prevent duplicate charges when agents retry. - Webhooks confirm settlement, not just payment initiation. Wait for the settlement webhook before fulfilling orders.
- Verify webhook signatures every time—this is your security layer against spoofed payments.
- Use
await_settlement()with a timeout rather than blocking indefinitely. Combine with retry logic for robustness. - AgentPay never holds money. Your merchant bank account receives funds directly. Inspect the MIT-licensed code at GitHub to verify.
- MCP server makes Claude payment-native. Configure once, and agents can collect payments autonomously.
Getting Started
Ready to build agent-native payments?
Install AgentPay VN:
pip install agentpay-vn
Review full documentation: https://agentpay.servicesai.vn/v1/docs
Explore the open-source code: https://github.com/phuocdu/agentpay-vn
Next steps:
1. Get an API key from the docs.
2. Test create_payment_request() in your local environment.
3. Deploy a webhook handler (Flask, FastAPI, or your framework).
4. Configure the MCP server if using Claude.
5. Run a test transaction end-to-end (use sandbox mode if available).
Your AI agents are ready to monetize. Build with confidence, knowing payments won't ghost you.