Agent-Native Payments: Idempotency, Webhooks & Settlement
The Problem: Why Your AI Agent's Payment Flow Keeps Failing
Imagine your course-selling chatbot just processed a student's enrollment. The agent calls your payment API, gets a temporary network hiccup, retries, and suddenly the student's card is charged twice—but the agent doesn't know it. Or worse: the webhook confirming settlement never arrives, so your bot sits there indefinitely, unable to unlock the course material.
This is the harsh reality of AI agents handling money. Unlike traditional web apps where a human refreshes a page or sees an error, agents operate autonomously. They need idempotency (doing the same thing twice produces the same result), reliable webhooks (knowing when payment actually settled), and settlement confirmation (proof the merchant's bank account received funds).
AgentPay VN solves this with a three-line flow backed by VietQR infrastructure. But to use it correctly—especially in production—you need to understand the why behind each pattern.
Why Idempotency Matters for Autonomous Systems
Idempotency isn't just a nice-to-have; it's fundamental to agent reliability.
When your agent creates a payment request, it might: - Send the request successfully but lose the response (network timeout) - Receive the response but crash before saving the payment ID - Retry due to an internal error, accidentally creating two requests
Without idempotency, your bot could generate duplicate invoices or charge a customer twice.
AgentPay VN prevents this by accepting an idempotency key—a unique identifier you provide. Send the same key twice, and the server returns the same payment request both times:
from agentpay_vn import AgentPayClient
# Initialize the client (reads AGENTPAY_API_KEY from env)
client = AgentPayClient()
# Create a payment request for a course enrollment
# The idempotency_key ensures this request is unique per student per course
idempotency_key = f"course_enrollment_{student_id}_{course_id}_{datetime.now().isoformat()}"
payment = client.create_payment_request(
amount=299000, # 299,000 VND for a Python course
description="Python Advanced Course - Q4 2024",
merchant_id="your_merchant_id", # Your VietQR merchant ID
idempotency_key=idempotency_key, # Critical: prevents duplicate charges
metadata={
"student_id": student_id,
"course_id": course_id,
"enrollment_timestamp": datetime.now().isoformat()
}
)
print(f"Payment ID: {payment.id}")
print(f"Checkout URL: {payment.checkout_url}")
# Output:
# Payment ID: pay_abc123xyz
# Checkout URL: https://vietqr.io/pay/abc123xyz
Key insight: Even if your agent retries this call 10 times with the same idempotency_key, it gets back pay_abc123xyz every time. The checkout URL remains constant. No duplicate charges.
Webhooks: The Agent's Eyes and Ears
An agent can't camp out refreshing a database waiting for settlement. Instead, you configure webhooks—HTTP callbacks that AgentPay VN fires when payment status changes.
Three critical events exist:
payment.created– Payment request was generated (student got the QR code)payment.confirmed– Student scanned and submitted the QR (confirmation pending)payment.settled– Funds hit your merchant bank account (final, irreversible)
Your agent should act only after receiving the payment.settled webhook. Before that, the payment is still in limbo.
Setting Up Your Webhook Receiver
Create a simple FastAPI endpoint to handle incoming webhooks:
from fastapi import FastAPI, Request, HTTPException
import hmac
import hashlib
import json
from datetime import datetime
app = FastAPI()
WEBHOOK_SECRET = "your_webhook_secret_from_agentpay_dashboard" # Store in env
@app.post("/webhooks/agentpay")
async def handle_webhook(request: Request):
"""
Receive and verify webhooks from AgentPay VN.
Every webhook is signed with HMAC-SHA256 for security.
"""
# 1. Read the raw body for signature verification
raw_body = await request.body()
signature = request.headers.get("X-AgentPay-Signature")
# 2. Verify HMAC signature (prevents spoofed webhooks)
expected_sig = hmac.new(
WEBHOOK_SECRET.encode(),
raw_body,
hashlib.sha256
).hexdigest()
if not hmac.compare_digest(signature, expected_sig):
raise HTTPException(status_code=401, detail="Invalid signature")
# 3. Parse the webhook payload
payload = json.loads(raw_body)
event_type = payload.get("event")
payment_data = payload.get("data", {})
payment_id = payment_data.get("id")
# 4. Route based on event type
if event_type == "payment.settled":
# ** This is where your agent's logic triggers **
# Fund transfer is confirmed—safe to deliver the course, ship product, etc.
student_id = payment_data.get("metadata", {}).get("student_id")
course_id = payment_data.get("metadata", {}).get("course_id")
# Example: unlock course for student
unlock_course_for_student(student_id, course_id)
# Mark payment as processed in your DB
save_settled_payment(
payment_id=payment_id,
student_id=student_id,
settled_at=datetime.fromisoformat(payment_data.get("settled_at")),
merchant_account=payment_data.get("merchant_account") # Proof of settlement
)
return {"status": "processed", "payment_id": payment_id}
elif event_type == "payment.confirmed":
# Student submitted payment; waiting for bank to clear it (usually < 1 minute)
log_payment_status(payment_id, "confirmed")
return {"status": "acknowledged"}
return {"status": "ignored"}
Critical pattern: Your agent (or webhook handler) should be idempotent too. If a webhook fires twice due to a network retry, calling unlock_course_for_student() twice should not break anything—use database checks like IF NOT EXISTS or deduplicate by payment_id.
Settlement Confirmation: Proof of Funds
Unlike payment gateways that hold money in escrow, AgentPay VN never holds your funds. The VietQR code points straight at your merchant's bank account. This is faster and more transparent, but you must confirm settlement yourself.
AgentPay VN provides two confirmation methods:
Method 1: Webhooks (Real-time, Recommended)
As shown above, payment.settled event includes proof:
{
"event": "payment.settled",
"data": {
"id": "pay_abc123xyz",
"amount": 299000,
"merchant_account": "0123456789",
"settled_at": "2024-11-15T14:32:10Z",
"bank_reference": "VT241115.1432.ABC123" // Proof from VietQR network
}
}
Store bank_reference in your database—it's your audit trail.
Method 2: Polling (Fallback)
If webhooks fail, your agent can poll for updates:
import asyncio
async def wait_for_settlement(payment_id: str, max_wait_seconds: int = 600):
"""
Poll AgentPay VN every 5 seconds to check if payment settled.
Timeout after 10 minutes (typical settlement is < 2 minutes).
"""
start_time = datetime.now()
while (datetime.now() - start_time).total_seconds() < max_wait_seconds:
payment = client.await_settlement(payment_id=payment_id)
if payment.status == "settled":
print(f"✓ Payment {payment_id} settled at {payment.settled_at}")
print(f" Bank reference: {payment.bank_reference}")
return payment
elif payment.status == "failed":
print(f"✗ Payment {payment_id} failed")
raise Exception(f"Payment failed: {payment.failure_reason}")
# Still pending; wait before retrying
await asyncio.sleep(5)
# Timeout—payment never settled
raise TimeoutError(f"Payment {payment_id} did not settle within {max_wait_seconds}s")
# Usage
await wait_for_settlement(payment_id="pay_abc123xyz")
Integrating with Claude via MCP Server
AgentPay VN includes an MCP (Model Context Protocol) server that lets Claude directly invoke payment operations. This is how your AI agent can autonomously handle payments.
Configure Claude's MCP
Add to your Claude desktop config (~/.claude/claude.json):
{
"mcpServers": {
"agentpay": {
"command": "python",
"args": ["-m", "agentpay_mcp"],
"env": {
"AGENTPAY_API_KEY": "your_api_key_here",
"AGENTPAY_MERCHANT_ID": "your_merchant_id",
"AGENTPAY_WEBHOOK_SECRET": "your_webhook_secret"
}
}
}
}
Now Claude can call tools like:
- create_payment_request(amount, description, metadata, idempotency_key)
- get_payment_status(payment_id)
- await_settlement(payment_id)
Example Claude prompt:
You are a course sales agent. When a customer enrolls:
1. Use create_payment_request to generate a VietQR invoice
2. Share the checkout_url with the customer
3. Use await_settlement to wait for payment (with a 10-minute timeout)
4. Once settled, send a confirmation email with course access details
Customer: "I want to enroll in the Python Advanced course (299,000 VND)"
Claude will orchestrate the entire flow autonomously, handling retries and idempotency.
Real-World Walkthrough: Online Course Enrollment Bot
Let's trace a complete flow:
Minute 0:00 – Student says to bot: "Enroll me in Python Advanced (299k VND)"
-
Bot creates payment (idempotent):
idempotency_key = "enrollment_user_123_course_456_2024-11-15T10:00:00" payment = create_payment_request( amount=299000, idempotency_key=idempotency_key, metadata={"student_id": 123, "course_id": 456} ) // Returns: pay_abc123xyz, checkout_url = https://vietqr.io/pay/abc123xyz -
Bot shares QR code with student (shown as image or link)
-
Student scans QR on their banking app and transfers 299,000 VND
-
Minute 0:15 – VietQR processes the transaction, AgentPay fires webhook:
POST /webhooks/agentpay event: "payment.confirmed" data: { id: "pay_abc123xyz", status: "confirmed" } -
Minute 0:45 – Bank clears funds to course platform's merchant account, AgentPay fires:
POST /webhooks/agentpay event: "payment.settled" data: { id: "pay_abc123xyz", status: "settled", settled_at: "2024-11-15T10:00:45Z", bank_reference: "VT241115.1000.ABC123" } -
Bot's webhook handler: - Verifies HMAC signature ✓ - Extracts student_id=123, course_id=456 - Unlocks course access in database - Sends email: "Welcome! Your course is ready: [access link]"
-
Minute 0:46 – Student receives email and clicks link; course dashboard loads ✓
Total time: ~46 seconds from QR scan to course access.
Do's and Don'ts: Common Pitfalls
| Do | Don't |
|---|---|
Always provide an idempotency_key when creating payments |
Retry without changing the idempotency_key; AgentPay will return the same payment |
Store bank_reference from settled webhooks for audit trails |
Trust only the payment.created event; funds aren't confirmed yet |
Verify webhook HMAC signatures with hmac.compare_digest() |
Use string comparison (==) for signatures; vulnerable to timing attacks |
| Implement webhook retry logic on your server (e.g., return 5xx if processing fails) | Fire fulfillment logic before receiving payment.settled webhook |
| Set webhook timeout to 30+ seconds and retry for 24 hours | Assume a payment is settled just because the QR was scanned |
Use await_settlement() as a fallback, with a max_wait timeout |
Poll indefinitely without a timeout; could hang your agent forever |
FAQ: Idempotency, Webhooks, and Settlement
Q: What if my webhook handler crashes after verifying the signature but before unlocking the course?
A: AgentPay will retry the webhook for 24 hours. Your handler should be idempotent—e.g., use INSERT ... ON CONFLICT DO NOTHING in SQL so the second retry does no harm. Alternatively, track processed webhook IDs in a separate table.
Q: How long does settlement actually take?
A: Typically 30 seconds to 2 minutes after the student submits payment. VietQR operates on real-time bank transfers. The payment.confirmed event fires immediately; payment.settled follows once your bank confirms the incoming transfer.
Q: Can I use the same idempotency_key for multiple customers?
A: No. Each payment should have a unique key. A good pattern is f"{business_logic}_{unique_id}_{timestamp}", e.g., course_enrollment_user_123_2024-11-15T10:00:00. This ensures each customer's payment is distinct.
Q: What if AgentPay's webhook endpoint is down?
A: AgentPay will retry every 5 minutes for 24 hours. You'll eventually receive the webhook. In the meantime, your agent can use await_settlement() as a fallback to poll AgentPay directly.
Q: How does AgentPay verify it's my merchant account receiving funds?
A: During onboarding, you register your bank account with AgentPay. Every VietQR code is tied to your merchant ID and directs payments to that account. AgentPay doesn't hold or route funds—it just confirms the transfer happened via bank feeds.
Key Takeaways
- Idempotency is non-negotiable for autonomous agents; always provide a unique
idempotency_keywhen creating payments to prevent duplicate charges. - Webhooks are your source of truth, not polling. Act only after receiving
payment.settledevent; before that, the payment is still in limbo. - Settlement confirmation requires proof: store the
bank_referencefrom webhooks; it's your audit trail proving funds reached your merchant account. - AgentPay never holds money—the QR points straight to your bank. This is faster and simpler but means you're responsible for confirming settlement yourself.
- HMAC signature verification is critical: use
hmac.compare_digest()to prevent timing attacks; always verify webhook authenticity. - Webhook idempotency matters too: design your handlers (course unlock, email sends) to be safe if called multiple times with the same payment ID.
- Polling is a fallback: only use
await_settlement()if webhooks fail; set a reasonable timeout (e.g., 10 minutes) to avoid hanging agents.
Get Started Today
Ready to build AI agents that collect payments reliably? Here's what you need:
-
Install AgentPay VN SDK:
bash pip install agentpay-vn -
Get your API key from the AgentPay VN dashboard and set
AGENTPAY_API_KEYenv var. -
Review the full documentation at https://agentpay.servicesai.vn/v1/docs for webhook setup, MCP integration, and production best practices.
-
Check out the source code on GitHub for examples and contribution guidelines.
Your AI agent can now collect payments with the reliability of a professional payment processor—autonomously, securely, and without holding customer funds. That's the AgentPay VN difference.