Agent-Native Payments: Idempotency, Webhooks & Settlement
The Silent Killer of Agent Payment Flows
It's 2 AM. Your AI chatbot has just processed 47 payment requests for an online course platform. Everything seemed smooth—customers clicked, QR codes appeared, money landed in the account. Then your team discovers something horrifying: three customers were charged twice. Not by accident. By network hiccup.
Your agent, receiving no confirmation webhook, retried the payment creation. The SDK created a second request. Two identical QR codes. Two settlements. Refunds, apologies, reputation damage.
This is the nightmare that haunts production payment systems—and it's exactly why idempotency, webhooks, and settlement confirmation exist. If you're building AI agents that handle real money through VietQR, you need to understand these three pillars. AgentPay VN makes them accessible, but you still need to wire them correctly.
Let's dig in.
Why Agents Break Payment Systems (And How to Fix It)
AI agents are relentless retry machines. They hallucinate, timeout, recover, and try again. A traditional web form doesn't retry payment creation. An agent does—dozens of times, if you let it.
Here's what happens without idempotency:
- Agent calls
create_payment_request() - Request succeeds; QR code is generated
- Network glitches before response reaches agent
- Agent doesn't know it worked, retries
- Second identical request is processed
- Chaos.
AgentPay VN solves this with idempotency keys—a unique identifier per logical action that tells the server: "If you've already done this, don't do it again." Same key, same response, always.
The second pillar is webhooks: your backend listening for settlement confirmations from the bank. Without this, you're polling or guessing. With it, you're certain.
The third is settlement flow awareness. VietQR payments don't settle instantly. A customer scans the QR, their bank deducts funds, your merchant bank receives them—but there's a window (usually minutes to hours). Your agent needs to know the difference between "payment requested," "payment received," and "funds settled."
AgentPay VN was built for this. It holds no money—the QR points straight at your merchant bank account. A bank feed confirms settlement. This is elegant and bulletproof. But you must implement the flow correctly.
The Three-Step Agent Payment Flow
Here's the happy path:
from agentpay_vn import AgentPayClient, PaymentRequest
import uuid
# Initialize the client (configure with your merchant bank details)
client = AgentPayClient(
api_key="your_api_key",
bank_account="your_vietqr_bank_account"
)
# Step 1: Create a payment request with idempotency
# The idempotency_key ensures this exact request happens only once
idempotency_key = f"course_purchase_{uuid.uuid4()}"
payment_request = client.create_payment_request(
amount=499000, # VND
description="Online Course: Python for AI",
idempotency_key=idempotency_key,
metadata={
"customer_id": "user_12345",
"course_id": "python_ai_101",
"agent_session_id": "agent_sess_67890"
}
)
print(f"QR Code URL: {payment_request.checkout_url}")
print(f"Payment ID: {payment_request.id}")
# Step 2: Send the checkout URL to the customer (via agent)
# The agent can now safely present this to the user
agent_message = f"Click here to pay: {payment_request.checkout_url}"
# Step 3: Wait for settlement (with timeout and polling strategy)
import time
from datetime import datetime, timedelta
MAX_WAIT_SECONDS = 900 # 15 minutes
POLL_INTERVAL_SECONDS = 5
start_time = datetime.now()
while datetime.now() - start_time < timedelta(seconds=MAX_WAIT_SECONDS):
settlement = client.await_settlement(
payment_id=payment_request.id,
timeout_seconds=POLL_INTERVAL_SECONDS
)
if settlement.status == "settled":
print(f"✓ Payment settled! Amount: {settlement.amount} VND")
print(f"Settlement ID: {settlement.settlement_id}")
# Grant access to course
grant_course_access("user_12345", "python_ai_101")
break
elif settlement.status == "failed":
print(f"✗ Payment failed: {settlement.error_reason}")
break
else:
# Status: pending, awaiting_confirmation, etc.
print(f"Awaiting settlement... (status: {settlement.status})")
time.sleep(POLL_INTERVAL_SECONDS)
else:
print("Timeout waiting for settlement")
Notice three critical details:
-
Idempotency key: Unique per transaction. If your agent retries (network glitch, timeout, whatever), that exact same key is sent again. AgentPay VN recognizes it: "Oh, I've seen this before. Here's the same response." No double charge.
-
Polling with backoff: We're not blocking forever. We poll every 5 seconds, for up to 15 minutes. This lets your agent move on while staying responsive.
-
Metadata: We embed the customer ID, course ID, and agent session ID. This is your breadcrumb trail for debugging and linking settlements to business logic.
Webhooks: Never Poll Again
Polling works but is inefficient. A webhook is better: your backend listens for settlement notifications and reacts instantly.
Set up your webhook endpoint first. Here's a minimal Flask example:
from flask import Flask, request
import hmac
import hashlib
app = Flask(__name__)
WEBHOOK_SECRET = "your_webhook_secret_from_agentpay"
@app.route("/webhooks/agentpay", methods=["POST"])
def handle_settlement_webhook():
# Verify the signature to ensure the request is genuine
payload = request.get_data(as_text=True)
signature = request.headers.get("X-AgentPay-Signature")
expected_sig = hmac.new(
WEBHOOK_SECRET.encode(),
payload.encode(),
hashlib.sha256
).hexdigest()
if not hmac.compare_digest(signature, expected_sig):
return {"error": "Invalid signature"}, 403
# Parse the event
event = request.json
payment_id = event["payment_id"]
settlement_id = event["settlement_id"]
status = event["status"] # e.g., "settled"
amount = event["amount"]
if status == "settled":
# Grant access / complete order
customer_id = event["metadata"]["customer_id"]
course_id = event["metadata"]["course_id"]
grant_course_access(customer_id, course_id)
log_settlement(payment_id, settlement_id, customer_id, amount)
return {"ok": True}, 200
if __name__ == "__main__":
app.run(port=5000)
Now register this webhook with AgentPay VN in your dashboard or via API:
# Example: register webhook (check docs for exact endpoint)
curl -X POST https://agentpay.servicesai.vn/v1/webhooks \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://yourdomain.com/webhooks/agentpay",
"events": ["settlement.completed", "settlement.failed"]
}'
With webhooks, your agent doesn't need to poll. It sends the payment request, includes a callback URL in metadata, and your backend reacts the moment settlement is confirmed. Faster, cheaper, cleaner.
Idempotency Deep Dive: Why It Matters
Idempotency is not optional for agents. Here's why:
Scenario 1: Network timeout
Agent creates payment request, network times out before response. Agent doesn't know if it succeeded. It retries with the same idempotency key. AgentPay VN returns the original response instantly. No duplicate payment.
Scenario 2: Agent hallucination / retry logic
Your agent's internal reasoning loop decides to retry a failed step. It calls create_payment_request() again. Same idempotency key (because it's still the same logical transaction). Same QR code is returned. Safe.
Scenario 3: Multi-step agent workflow
Your agent is orchestrating a multi-step order (verify email → charge → send receipt). If the agent crashes after charging but before sending receipt, it resumes. The idempotency key is already known, so recreating the payment request is safe.
How do you generate idempotency keys? Use a deterministic, unique identifier tied to the transaction:
import hashlib
# Good: Tied to order ID and customer
order_id = "order_2024_01_12345"
customer_id = "cust_789"
idempotency_key = hashlib.sha256(
f"{order_id}_{customer_id}".encode()
).hexdigest()[:32]
# Bad: Random UUID (agent can't reproduce it if it retries)
idempotency_key = str(uuid.uuid4()) # Don't do this
With deterministic keys, your agent can always re-derive the correct idempotency key, even after a restart.
Building Agent Payment Logic with MCP
If you're using Claude or another LLM-based agent framework, AgentPay VN ships an MCP (Model Context Protocol) server that exposes payment functions directly.
Configure your MCP client (e.g., in Claude's claude_desktop_config.json):
{
"mcpServers": {
"agentpay": {
"command": "python",
"args": ["-m", "agentpay_mcp"],
"env": {
"AGENTPAY_API_KEY": "your_api_key",
"AGENTPAY_BANK_ACCOUNT": "your_bank_account",
"AGENTPAY_WEBHOOK_SECRET": "your_webhook_secret"
}
}
}
}
Now Claude can call payment functions natively. In a conversation:
User: "I want to buy the Python course for 499,000 VND."
Claude: I'll create a payment request for you.
[Calls create_payment_request via MCP]
Claude: Here's your payment link: [QR checkout URL].
Once you scan and pay, I'll confirm your enrollment.
[Awaits webhook or polling for settlement]
Claude: ✓ Payment confirmed! Access granted to "Python for AI".
Check your email for the course link.
The MCP server handles all the SDK boilerplate. Your agent focuses on conversation logic.
Real-World Walkthrough: Online Course Bot
Let's build a complete example: an AI agent that sells online courses via VietQR.
The flow:
- User messages: "I want to buy the Python course."
- Agent verifies course availability and pricing (499,000 VND).
- Agent creates a payment request with the user's email and course ID.
- Agent presents the checkout URL (as a QR code or link).
- User scans and pays via their bank app.
- Webhook fires; backend grants course access and emails the download link.
- Agent confirms: "You're enrolled! Check your email."
Implementation:
# In your agent's tool library
from agentpay_vn import AgentPayClient
import os
class CoursePaymentTool:
def __init__(self):
self.client = AgentPayClient(
api_key=os.getenv("AGENTPAY_API_KEY"),
bank_account=os.getenv("AGENTPAY_BANK_ACCOUNT")
)
self.courses = {
"python_ai_101": {"name": "Python for AI", "price": 499000},
"web_dev_201": {"name": "Web Dev Mastery", "price": 799000},
}
def initiate_purchase(self, course_id: str, customer_email: str) -> dict:
"""Agent calls this to start a purchase."""
if course_id not in self.courses:
return {"error": "Course not found"}
course = self.courses[course_id]
# Create payment with idempotent key
import hashlib
idempotency_key = hashlib.sha256(
f"{customer_email}_{course_id}_purchase".encode()
).hexdigest()[:32]
payment = self.client.create_payment_request(
amount=course["price"],
description=f"Enrollment: {course['name']}",
idempotency_key=idempotency_key,
metadata={
"customer_email": customer_email,
"course_id": course_id,
"agent_action": "course_enrollment"
}
)
return {
"payment_id": payment.id,
"checkout_url": payment.checkout_url,
"course_name": course["name"],
"amount_vnd": course["price"]
}
# Webhook handler logs the settlement and grants access
@app.route("/webhooks/course_payment", methods=["POST"])
def handle_course_payment():
# ... signature verification (as above) ...
event = request.json
if event["status"] == "settled":
customer_email = event["metadata"]["customer_email"]
course_id = event["metadata"]["course_id"]
# Grant course access
db.users.update_one(
{"email": customer_email},
{"$push": {"enrolled_courses": course_id}}
)
# Send confirmation email
send_enrollment_email(
customer_email,
course_id,
download_link=generate_course_link(course_id)
)
return {"ok": True}, 200
The agent calls initiate_purchase(), presents the checkout URL, and the webhook backend handles the rest.
Do's and Don'ts
| Do | Don't |
|---|---|
| ✓ Use deterministic idempotency keys tied to the transaction | ✗ Generate random UUIDs for idempotency keys |
| ✓ Verify webhook signatures using HMAC-SHA256 | ✗ Trust webhooks without validation |
| ✓ Implement exponential backoff when polling for settlement | ✗ Poll every second forever |
| ✓ Store payment IDs and settlement IDs in your database | ✗ Rely on in-memory state or session variables |
| ✓ Include metadata with customer and order IDs | ✗ Leave metadata empty; lose the audit trail |
| ✓ Handle both success and failure webhook events | ✗ Assume every webhook is a success |
| ✓ Test idempotency by retrying the same request | ✗ Skip testing retry scenarios |
| ✓ Use SDK version 2.0+ for full MCP support | ✗ Manually parse responses or ignore MCP |
FAQ
Q: What if a webhook is delayed or lost?
A: Webhooks are reliable but not guaranteed. Always implement a fallback polling mechanism for critical flows. Check settlement status at user login or after 10 minutes if no webhook arrives. AgentPay VN will deliver the webhook eventually; your polling is a safety net.
Q: Can I use the same idempotency key for different amounts?
A: No. AgentPay VN stores the full request (amount, description, metadata) per idempotency key. If you retry with the same key but different amount, the API will reject it or return the original request's amount. Always use consistent, deterministic keys.
Q: How long does settlement take?
A: Usually 5–30 minutes for VietQR. Some banks are slower. Your agent should be willing to wait 15 minutes before timing out. Use webhooks for better UX—don't make users wait for polling to finish.
Q: What if the customer scans the QR code twice?
A: The QR code is tied to a single payment request. Most banks will reject a duplicate payment attempt (double spending). But your idempotency key ensures that even if your agent accidentally creates two payment requests for the same logical transaction, only one succeeds.
Key Takeaways
- Idempotency keys prevent duplicate charges when agents retry. Use deterministic, transaction-tied keys that the agent can always re-derive.
- Webhooks are superior to polling for settlement confirmation. They're faster, cheaper, and more reliable. Implement both for redundancy.
- AgentPay VN never holds funds—the QR code points directly at your merchant bank account. This is safe by design; you just need to implement the flow correctly.
- Verify webhook signatures using HMAC-SHA256. Never trust incoming requests blindly.
- Metadata is your friend. Embed customer IDs, order IDs, and context. It's the breadcrumb trail for debugging and linking settlements to business logic.
- Test retries and network failures in staging. Real-world agents will encounter timeouts; make sure your idempotency logic survives them.
- Use the MCP server if you're building Claude-based agents. It abstracts away SDK boilerplate and lets your agent focus on conversation and business logic.
Getting Started
You're ready to build. Install AgentPay VN:
pip install agentpay-vn
Read the full docs and API reference:
👉 https://agentpay.servicesai.vn/v1/docs
Explore the source code and examples:
Remember: idempotency, webhooks, and settlement awareness are not optional. They're the foundation of agent-native payments that don't fail. Build them in from day one, and your payment flows will be bulletproof.