Agent-native payments: idempotency, webhooks & settlement
The Problem: Why Your AI Payment Agent Keeps Breaking
It's 2 AM. Your course-selling bot just processed the same $50 payment request three times because the network hiccupped, and your customer's bank showed three pending transactions. Meanwhile, your agent is sitting in a retry loop, unable to confirm if money actually landed in your account—so it can't auto-unlock the course materials.
This is the silent killer of agent-native payment systems. Unlike traditional APIs where a human notices something went wrong, AI agents running 24/7 can amplify problems exponentially. A flaky network, a timeout, or a momentary bank hiccup turns into a cascade of duplicate charges, stuck refunds, and customers demanding answers.
AgentPay VN solves this by building three critical patterns into its architecture: idempotency (same request, same result), webhook verification (prove the bank really called you), and settlement confirmation (know for certain when money landed). Let's walk through how to use them.
Understanding Idempotency: The Guard Rail Against Duplicates
Idempotency is a mathematical concept that means "doing something twice gives the same result as doing it once." For payments, it means:
Agent calls create_payment_request() with ID="order-12345"
→ AgentPay generates QR code, stores request
Agent calls same endpoint again with ID="order-12345"
→ AgentPay returns the *same* QR code (doesn't charge twice)
Without this, network retries = duplicate charges. With it, your agent can safely retry without fear.
How AgentPay VN Implements Idempotency
Every payment request must include an idempotency key—a unique identifier for that specific transaction. AgentPay uses this to detect if you're asking for the same payment twice:
from agentpay_vn import AgentPayClient
client = AgentPayClient(api_key="your_api_key")
# Create payment with idempotency key
payment = client.create_payment_request(
idempotency_key="order-12345-001", # Unique per transaction
amount_vnd=500000, # ₫500,000
merchant_account="0911123456", # VietQR account
description="Course: Python Fundamentals",
order_id="order-12345",
metadata={
"student_email": "alice@example.com",
"course_id": "py-101"
}
)
print(f"QR Code: {payment['checkout_url']}")
print(f"Request ID: {payment['request_id']}")
What happens on retry:
If your agent crashes after generating this QR but before storing it, on restart it calls the exact same code with the same idempotency_key. AgentPay returns the cached response—same QR, same request_id, zero additional bank interactions.
Best practice: Derive the idempotency key from something immutable. Use f"{order_id}-{attempt_counter}" or hash the order details. Don't use timestamps—those change on retry.
Webhooks: Proof the Bank Actually Paid You
Here's the gap many agents miss: just because AgentPay received a payment doesn't mean it's settled at your bank. VietQR flows through multiple systems:
- Customer scans QR → Bank app → Sends transfer
- ACH system processes transfer (can take seconds to hours)
- Your bank receives and credits your account
- Your bank (optionally) calls your webhook to confirm
Without webhook verification, your agent might unlock a course while the bank is still deciding whether the transfer is legitimate.
Setting Up Webhook Verification
AgentPay sends signed webhooks. Your agent must verify the signature:
from agentpay_vn import AgentPayClient
import hmac
import hashlib
import json
client = AgentPayClient(api_key="your_api_key")
def verify_webhook(payload_bytes, signature_header, webhook_secret):
"""
Verify that the webhook came from AgentPay, not an attacker.
Args:
payload_bytes: Raw request body (bytes)
signature_header: Value from X-AgentPay-Signature header
webhook_secret: Your webhook secret from dashboard
Returns:
True if signature is valid, False otherwise
"""
expected_signature = hmac.new(
webhook_secret.encode(),
payload_bytes,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature_header, expected_signature)
def handle_webhook(request_body, headers):
"""
Example webhook handler (Flask-like pseudocode).
"""
payload_bytes = request_body if isinstance(request_body, bytes) else request_body.encode()
signature = headers.get('X-AgentPay-Signature')
webhook_secret = "your_webhook_secret"
# Verify signature before processing
if not verify_webhook(payload_bytes, signature, webhook_secret):
return {"error": "Invalid signature"}, 401
# Parse payload
event = json.loads(payload_bytes)
if event['type'] == 'settlement.confirmed':
request_id = event['data']['request_id']
amount = event['data']['amount_vnd']
# Safe to act: money is confirmed at your bank
print(f"✓ Settlement confirmed: {request_id} = ₫{amount}")
# Now unlock course, send email, etc.
unlock_course_for_order(request_id)
return {"status": "processed"}, 200
return {"status": "ignored"}, 200
Webhook event types you'll see:
- settlement.pending: Transfer initiated, waiting for bank
- settlement.confirmed: Money is in your account ✓
- settlement.failed: Transfer rejected (insufficient funds, wrong account, etc.)
Critical: Always verify the signature. An attacker could call your webhook endpoint and claim payment was successful.
Settlement Confirmation: Polling vs. Webhooks
Not all environments support webhooks (some VPCs, legacy systems). For those cases, AgentPay provides polling:
import asyncio
async def wait_for_settlement(request_id, timeout_seconds=300, poll_interval=5):
"""
Poll AgentPay for settlement status instead of waiting for webhook.
Use this if:
- Your environment can't receive incoming webhooks
- You want immediate confirmation (webhooks have latency)
- You're running a synchronous agent
"""
start_time = asyncio.get_event_loop().time()
while asyncio.get_event_loop().time() - start_time < timeout_seconds:
status = client.check_payment_status(request_id=request_id)
if status['state'] == 'settled':
print(f"✓ Payment settled: {status['amount_vnd']} VND")
return {
"settled": True,
"amount": status['amount_vnd'],
"timestamp": status['settled_at']
}
elif status['state'] == 'failed':
print(f"✗ Payment failed: {status['failure_reason']}")
return {"settled": False, "reason": status['failure_reason']}
print(f"⏳ Still pending... (checked {status['state']})")
await asyncio.sleep(poll_interval)
raise TimeoutError(f"Payment {request_id} not settled after {timeout_seconds}s")
# Usage in an agent
async def sell_course(student_email, course_id, amount_vnd):
# Step 1: Create payment
payment = client.create_payment_request(
idempotency_key=f"course-{course_id}-{student_email}-{int(time.time())}",
amount_vnd=amount_vnd,
merchant_account="0911123456",
description=f"Course: {course_id}",
metadata={"student_email": student_email, "course_id": course_id}
)
# Step 2: Send QR to student
print(f"Pay here: {payment['checkout_url']}")
# Step 3: Wait for settlement (polling)
result = await wait_for_settlement(payment['request_id'], timeout_seconds=600)
if result['settled']:
print(f"Unlocking course for {student_email}")
unlock_course(student_email, course_id)
else:
print(f"Payment failed: {result['reason']}")
Real-World Walkthrough: Online Café Shop
Let's build a micro e-commerce bot for a coffee roastery:
Scenario: Customer orders ₫200k coffee beans online. The bot should: 1. Generate a unique payment request (idempotent) 2. Send the QR code via email 3. Confirm settlement before shipping 4. Handle retries gracefully
from agentpay_vn import AgentPayClient
import uuid
import time
client = AgentPayClient(api_key="demo_key")
class CoffeeShop:
def __init__(self):
self.merchant_account = "0911123456" # Your VietQR account
self.webhook_secret = "demo_secret"
def create_order(self, customer_email, product_id, amount_vnd):
"""
Create and pay for an order.
"""
order_id = f"order-{uuid.uuid4().hex[:8]}"
idempotency_key = f"{order_id}-{int(time.time())}"
# Step 1: Create payment request
payment = client.create_payment_request(
idempotency_key=idempotency_key,
amount_vnd=amount_vnd,
merchant_account=self.merchant_account,
description=f"Coffee beans - {product_id}",
order_id=order_id,
metadata={
"customer_email": customer_email,
"product_id": product_id,
"shop": "The Daily Grind"
}
)
# Step 2: Send QR to customer
self.send_payment_email(customer_email, payment['checkout_url'])
# Step 3: Store order in database
self.store_order({
"order_id": order_id,
"request_id": payment['request_id'],
"customer_email": customer_email,
"amount": amount_vnd,
"status": "awaiting_payment",
"created_at": time.time()
})
return order_id, payment['checkout_url']
def send_payment_email(self, email, qr_url):
print(f"📧 Email to {email}: {qr_url}")
# Integration with SendGrid, Mailgun, etc.
def store_order(self, order):
print(f"💾 Storing order {order['order_id']}")
# Save to database
def ship_order(self, order_id):
print(f"📦 Shipping order {order_id}")
# Call shipping API, print label, etc.
# Usage:
shop = CoffeeShop()
order_id, qr_url = shop.create_order(
customer_email="alice@example.com",
product_id="ethiopian-natural",
amount_vnd=200000
)
print(f"Order created: {order_id}")
print(f"Customer scans: {qr_url}")
# Later, when webhook arrives (settlement.confirmed):
# → shop.ship_order(order_id)
Do's and Don'ts: Common Pitfalls
| ✅ DO | ❌ DON'T |
|---|---|
| Use an idempotency key per transaction | Use random UUID every retry (defeats idempotency) |
Verify webhook signatures with hmac.compare_digest() |
Trust webhook without checking signature |
| Poll with exponential backoff (5s → 10s → 20s) | Poll every millisecond (hammers the API) |
Lock orders as awaiting_payment until webhook |
Release course/ship item immediately after QR |
Store request_id to track settlement |
Only store order ID, lose ability to query status |
| Test webhooks in staging before production | Deploy webhook handler untested |
Handle settlement.failed events (retry logic) |
Ignore failures, assume all transfers succeed |
Setting Up the MCP Server for Claude
If you're using Claude as your agent, AgentPay provides an MCP (Model Context Protocol) server for seamless integration:
1. Install MCP server:
pip install agentpay-mcp
2. Configure Claude (in claude_desktop_config.json):
{
"mcpServers": {
"agentpay": {
"command": "python",
"args": ["-m", "agentpay_mcp.server"],
"env": {
"AGENTPAY_API_KEY": "your_api_key",
"AGENTPAY_WEBHOOK_SECRET": "your_webhook_secret"
}
}
}
}
3. Now Claude can call these tools natively:
Claude: "The customer paid, let me confirm settlement."
[Claude calls tool: agentpay.check_payment_status]
Result: {"state": "settled", "amount_vnd": 500000}
No SDK imports needed—Claude handles it.
FAQ: Settling Common Questions
Q: What if my webhook server is down when AgentPay tries to call it?
A: AgentPay retries webhooks for 24 hours (exponential backoff). Use polling as a fallback—call check_payment_status() regularly to catch settled payments even if webhooks failed.
Q: Can I charge the customer's account directly without a QR code?
A: No. VietQR is a pull-based protocol—the customer initiates the transfer via their bank app. AgentPay never holds money or accesses accounts. The QR points straight at your merchant account; the customer's bank completes the transfer.
Q: How long does settlement take?
A: Typically 30 seconds to 5 minutes after the customer confirms. Some banks may take up to 24 hours during peak times. Set your polling timeout accordingly (timeout_seconds=600 = 10 minutes is safe).
Q: What if a customer disputes a charge after settlement?
A: Disputes go through your bank's chargeback process (standard banking practice). AgentPay provides webhook events for reversals (settlement.reversed). Implement refund logic if the customer paid twice or the product failed to deliver.
Key Takeaways
- Idempotency keys prevent duplicate charges when your agent retries (the single most important pattern for reliability)
- Webhook signatures prove the bank actually called you—never trust unsigned webhooks
- Polling is your fallback if webhooks fail; check status every few seconds until settled
- VietQR is pull-based: the customer sends money via their bank app; AgentPay never holds it
- AgentPay's MCP server lets Claude natively call payment tools without SDK imports
- Test settlement workflows in staging before production; use low amounts first
Next Steps
Ready to add agent-native payments to your app?
- Install the SDK:
pip install agentpay-vn - Read the docs: https://agentpay.servicesai.vn/v1/docs
- Clone the example: https://github.com/phuocdu/agentpay-vn
- Set up your webhook endpoint and test with the provided
test_webhook.pyscript - Configure your merchant account in the AgentPay dashboard
Start with a test payment (₫1,000) to verify the flow. Once idempotency, webhooks, and settlement confirmation are working, you can scale to production.
Happy agent-native selling! 🚀