Agent-Native Payments: Idempotency, Webhooks & Settlement
The Problem: Why Agents Fail at Handling Money
Imagine you've built an AI assistant that books online courses. A student clicks "enroll," the agent creates a payment request, and—network hiccup—the connection drops. The agent retries. And retries again.
Now you have three payment requests for the same $50 course.
Or worse: the webhook confirming settlement never arrives, so your agent refunds the student and marks the course as free. Meanwhile, the money actually landed in your bank account yesterday.
This is the harsh reality of agent-native payments. Unlike traditional checkout forms where a human manually confirms their action, AI agents operate at machine speed, in loops, sometimes without human oversight. They need guarantees: that duplicate requests won't create duplicate charges, that settlement confirmation is ironclad, and that failures are recoverable.
AgentPay VN solves this by treating the payment flow as idempotent, webhook-driven, and bank-settled from day one. No escrow. No holding funds. Money goes straight to your merchant account, and you know exactly when it's there.
Why Idempotency Is Non-Negotiable for Agents
Idempotency means: making the same request 100 times produces the same result as making it once.
In agent workflows, this is critical because:
- Network retries are inevitable. Your agent might lose connection mid-request.
- Agents loop. They check "did payment succeed?" repeatedly until they get confirmation.
- No human review. Unlike a person who notices they clicked a button twice, an agent doesn't.
AgentPay VN bakes idempotency in via idempotency keys. When you create a payment request, you pass a unique identifier (usually based on the order ID or user + timestamp). If your agent crashes and retries with the same key, the system returns the existing payment request instead of creating a duplicate.
Request 1: idempotency_key="user_123_order_5678_1703001600"
→ Creates payment QR, returns checkout_url
Request 2 (retry): idempotency_key="user_123_order_5678_1703001600"
→ Returns *the same* checkout_url (no new QR)
Request 3 (another retry): idempotency_key="user_123_order_5678_1703001600"
→ Still the same QR
The customer, if they've already paid, sees the same QR. If they haven't, they scan it and pay. One transaction. One settlement.
How Webhooks Lock In Settlement Certainty
After a customer scans the VietQR code and transfers money, two things happen:
- The bank processes the transfer (usually within seconds to a few minutes).
- AgentPay VN receives a webhook from your merchant bank, confirming the funds arrived.
Your agent should never mark a payment as "settled" based on a timer or a single polling check. Webhooks are the source of truth. They're cryptographically signed and timestamped by the bank.
This means: - You know settlement happened (not "probably" or "in a few hours"). - Your agent can immediately trigger downstream actions: send course access, ship product, add credits. - You have a permanent audit log of every settlement event.
Step-by-Step: Building an Idempotent Agent Payment Flow
Step 1: Install AgentPay VN
pip install agentpay-vn
Step 2: Create a Payment Request with Idempotency
Here's a real agent workflow—say, a café selling coffee subscriptions:
from agentpay_vn import AgentPayClient
import hashlib
import time
# Initialize the client (reads AGENTPAY_API_KEY from env)
client = AgentPayClient()
def create_subscription_payment(customer_id: str, amount_vnd: int, plan_name: str):
"""
Create a payment request for a coffee subscription.
Uses an idempotency key to prevent duplicates if the agent retries.
"""
# Step 1: Build a deterministic idempotency key
# This key uniquely identifies this customer + plan + time window
timestamp_bucket = int(time.time() / 60) * 60 # Round to nearest minute
idempotency_key = hashlib.sha256(
f"{customer_id}_{plan_name}_{timestamp_bucket}".encode()
).hexdigest()[:16]
# Step 2: Create the payment request
payment = client.create_payment_request(
amount=amount_vnd,
description=f"Coffee subscription: {plan_name}",
idempotency_key=idempotency_key,
metadata={
"customer_id": customer_id,
"plan": plan_name,
"subscription": True
}
)
# Step 3: Agent sends the checkout URL to the customer
print(f"📱 Share this with customer:\n{payment.checkout_url}")
print(f"🔑 Payment ID: {payment.id}")
return payment
# Agent usage:
payment = create_subscription_payment(
customer_id="cust_alice_001",
amount_vnd=99000, # ~$4 USD
plan_name="premium_monthly"
)
Line-by-line explanation:
- Lines 10–13: We create a deterministic idempotency key by hashing the customer ID, plan name, and a time bucket. If the agent retries within the same minute, it uses the same key.
- Lines 15–24: We call create_payment_request() with the idempotency key. If this key has been used before, the SDK returns the existing payment instead of creating a new one.
- Lines 27–30: We extract the checkout_url (the actual VietQR link) and send it to the customer. The agent now has a payment.id to track this transaction.
Step 3: Await Settlement via Webhook (Not Polling)
Don't do this:
# ❌ BAD: Polling in a loop
for i in range(60):
status = client.get_payment_status(payment.id)
if status == "settled":
break
time.sleep(5)
Do this instead:
def setup_settlement_webhook(agent_storage):
"""
Register a webhook endpoint to handle bank settlements.
This is the ground truth for payment confirmation.
"""
def on_settlement(webhook_payload):
payment_id = webhook_payload["payment_id"]
amount = webhook_payload["amount"]
timestamp = webhook_payload["settled_at"]
# Webhook is cryptographically verified by AgentPay VN
# Safe to trust this as the source of truth
# Step 1: Log the settlement
agent_storage.log_settlement(
payment_id=payment_id,
amount=amount,
timestamp=timestamp
)
# Step 2: Trigger agent action (e.g., activate subscription)
agent_storage.mark_subscription_active(payment_id)
# Step 3: Send confirmation to customer
customer_id = agent_storage.get_customer_by_payment(payment_id)
send_email(customer_id, f"Your payment of {amount:,} VND confirmed!")
return {"status": "ok"}
return on_settlement
Why webhooks are superior: - No lag. You act the moment the bank confirms. - No missed events. If you were offline polling, you'd miss the settlement. - Cryptographic trust. AgentPay VN signs each webhook; your agent verifies the signature before acting.
Configuring AgentPay VN as an MCP Server for Claude
If you're using Claude or another LLM agent, integrate AgentPay VN as an MCP (Model Context Protocol) server:
{
"name": "agentpay-mcp",
"version": "1.0",
"resources": [
{
"uri": "payment://create",
"mimeType": "application/json",
"description": "Create a new payment request",
"parameters": {
"amount": "integer (VND)",
"description": "string",
"idempotency_key": "string (unique identifier)",
"metadata": "object (optional, for tracking)"
}
},
{
"uri": "payment://settle",
"mimeType": "application/json",
"description": "Confirm settlement via webhook",
"parameters": {
"payment_id": "string",
"signature": "string (bank-signed)"
}
}
],
"tools": [
{
"name": "create_payment",
"description": "Create a VietQR payment request for an AI agent",
"inputSchema": {
"type": "object",
"properties": {
"amount": { "type": "number" },
"description": { "type": "string" },
"idempotency_key": { "type": "string" },
"metadata": { "type": "object" }
},
"required": ["amount", "description", "idempotency_key"]
}
}
]
}
This allows Claude to call AgentPay VN directly via the MCP protocol, treating payment creation as a native capability.
Real-World Walkthrough: An Online Course Bot
Let's build an end-to-end scenario: an AI agent selling Python courses.
User: "I want to buy the Advanced Django course."
Agent flow:
-
Create payment request (with idempotency key based on user + course):
amount = 299_000 VND idempotency_key = hash("user_bob_django_advanced") → Payment ID: pay_1a2b3c4d → Checkout URL: vietqr.co/... -
Send checkout link to user.
-
Customer scans QR with their banking app, transfers ₫299,000.
-
Bank settlement webhook fires (within 2 minutes typically):
json { "event": "payment.settled", "payment_id": "pay_1a2b3c4d", "amount": 299000, "settled_at": "2024-01-15T10:45:32Z", "signature": "...(bank-signed)..." } -
Agent receives webhook, verifies signature, and: - Marks payment as settled in the database. - Generates course access link. - Sends email: "Your course access: [link]. Enjoy!" - Logs the transaction for accounting.
-
If agent crashes during step 3 and retries creating the payment: - Uses same idempotency key → gets same payment ID. - User sees same QR. - No duplicate charge.
Idempotency vs. Webhooks: Do's and Don'ts
| ✅ DO | ❌ DON'T |
|---|---|
| Use deterministic idempotency keys based on user + item + time bucket | Rely on random UUIDs; they won't deduplicate on retry |
| Listen for webhooks and act on settlement | Poll get_payment_status() in a loop |
| Log webhook signatures for audit purposes | Assume webhook came from the bank without verification |
| Retry payment requests with the same idempotency key | Increment a counter and generate a new key on each retry |
| Store the payment ID and link it to your order ID | Discard the payment ID and try to re-query by amount |
| Set a reasonable timeout for settlement (e.g., 10 minutes) | Wait indefinitely; implement a fallback if webhook never arrives |
Advanced Tips for Production Agents
Tip 1: Implement a Settlement Timeout
Webhooks are reliable, but network issues happen. Set a fallback:
import asyncio
async def wait_for_settlement(payment_id: str, timeout_seconds: int = 300):
"""
Wait for settlement via webhook, but bail out after timeout.
"""
settlement_event = asyncio.Event()
# Register callback to fire when webhook arrives
def on_webhook(event):
if event["payment_id"] == payment_id:
settlement_event.set()
# Subscribe to webhook
client.on_settlement(on_webhook)
try:
await asyncio.wait_for(
settlement_event.wait(),
timeout=timeout_seconds
)
return {"status": "settled", "via": "webhook"}
except asyncio.TimeoutError:
# Fallback: check payment status directly
status = client.get_payment_status(payment_id)
if status.settled:
return {"status": "settled", "via": "fallback_check"}
else:
return {"status": "pending", "via": "timeout"}
Tip 2: Use Exponential Backoff for Retries
def create_payment_with_retry(amount, description, idempotency_key, max_attempts=3):
"""
Retry payment creation with exponential backoff.
The idempotency key ensures no duplicates even after multiple retries.
"""
import time
for attempt in range(max_attempts):
try:
return client.create_payment_request(
amount=amount,
description=description,
idempotency_key=idempotency_key
)
except Exception as e:
if attempt < max_attempts - 1:
wait_time = 2 ** attempt # 1s, 2s, 4s
print(f"Attempt {attempt + 1} failed. Retrying in {wait_time}s...")
time.sleep(wait_time)
else:
raise
Tip 3: Webhook Signature Verification
Always verify that webhooks come from AgentPay VN:
import hmac
import hashlib
def verify_webhook_signature(payload_bytes, signature_header, secret_key):
"""
Verify that the webhook was signed by AgentPay VN.
"""
expected_signature = hmac.new(
secret_key.encode(),
payload_bytes,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature_header, expected_signature)
FAQ
Q: If I use the same idempotency key twice, will I be charged twice?
No. The SDK ensures that identical idempotency keys return the same payment request. The customer still pays only once, and the transaction settles once. Idempotency protects against duplicate charges caused by network retries or agent loops.
Q: What if the webhook never arrives?
Implement a timeout and fallback check (see Tip 1 above). After ~10 minutes, if the webhook hasn't arrived, your agent can call client.get_payment_status(payment_id) to check if the bank has confirmed settlement. The webhook is the preferred method because it's instant, but the fallback ensures you don't leave money on the table.
Q: Do you hold my money in escrow?
No. AgentPay VN is a payment flow coordinator, not a wallet. The VietQR points directly at your merchant bank account. When a customer transfers money, it lands in your account, not ours. A webhook from your bank confirms settlement. We never touch the funds.
Q: Can I use AgentPay VN outside Vietnam?
AgentPay VN uses VietQR, which is Vietnam's domestic QR code standard. It works for customers with Vietnamese bank accounts. International agents can initiate payments, but the payer must be in Vietnam with a compatible bank account.
Key Takeaways
- Idempotency keys prevent duplicate charges when agents retry payment requests. Use deterministic keys (e.g., hashed customer ID + order ID + timestamp bucket).
- Webhooks are the source of truth for settlement, not polling or timers. They're cryptographically signed by your bank and arrive within seconds of a transfer.
- AgentPay VN never holds funds. Money goes straight to your merchant account. Webhooks confirm when it lands.
- Implement timeouts and fallback checks for production robustness, even though webhooks are reliable.
- Verify webhook signatures before trusting any settlement event.
- Log payment IDs and link them to your orders so you can reconcile transactions with your business logic.
Get Started Now
Ready to add idempotent, webhook-driven payments to your AI agent?
-
Install AgentPay VN:
bash pip install agentpay-vn -
Explore the full documentation: https://agentpay.servicesai.vn/v1/docs
-
Check out the open-source code (MIT license): https://github.com/phuocdu/agentpay-vn
Your agents can now collect payments safely, reliably, and at scale. No more duplicate charges. No more mystery about settlement. Just pure, idempotent, webhook-driven Vietnamese payment flow.