Agent-Native Payments: Idempotency, Webhooks & Settlement
The Unreliable Agent Problem
You've built an AI agent that sells digital courses. A customer asks your agent: "I want to enroll in the Python course." The agent creates a payment request and sends the checkout URL. But then—the customer's network hiccups. The agent doesn't receive confirmation. Does it create another payment request? Now the customer sees two identical QR codes. They pay twice. Your support team gets a panicked message at 2 AM.
This scenario repeats across thousands of AI agent integrations. Without idempotency and proper settlement tracking, payment systems become liability machines, not revenue engines. AgentPay VN solves this with a bank-aware architecture: the QR points directly at the merchant's bank account, webhooks confirm settlement in real time, and idempotency keys prevent accidental duplicate charges.
Let's build a payment system that AI agents can trust.
Understanding Idempotency in Agent Workflows
Why Agents Break Payment Systems
Traditional payment integrations assume a synchronous, single-request user flow. An agent works differently:
- Retry loops: On network failure, agents retry automatically. Without idempotency, retry #3 creates a third charge.
- Parallel execution: Multi-agent systems may submit overlapping requests for the same order.
- State uncertainty: Agents operate in uncertain environments. They can't reliably distinguish between "payment created" and "payment creation failed mid-flight."
Idempotency is the fix. An idempotency key—a unique identifier you provide—ensures that identical requests produce identical results, no matter how many times they're sent.
AgentPay VN's Idempotency Model
AgentPay VN requires an idempotency_key in every create_payment_request call. This key is scoped to your merchant account, so you can safely retry without side effects:
from agentpay_vn import PaymentClient
client = PaymentClient(api_key="your_api_key")
# First request with idempotency key
response = client.create_payment_request(
amount=500000, # 500,000 VND
description="Python Course - Intermediate Level",
idempotency_key="order_user123_2025_01_15_attempt_1", # Unique per transaction
metadata={
"student_id": "user_123",
"course_id": "python_intermediate",
"agent_id": "enrollment_bot_v2"
}
)
checkout_url = response["checkout_url"]
print(f"Send this URL to customer: {checkout_url}")
# Network fails. Agent retries with the SAME idempotency key.
# AgentPay returns the same response—no duplicate charge.
retry_response = client.create_payment_request(
amount=500000,
description="Python Course - Intermediate Level",
idempotency_key="order_user123_2025_01_15_attempt_1", # Same key
metadata={
"student_id": "user_123",
"course_id": "python_intermediate",
"agent_id": "enrollment_bot_v2"
}
)
# retry_response["checkout_url"] == checkout_url
Line-by-line breakdown:
PaymentClientinitializes with your API key (from https://agentpay.servicesai.vn/v1/docs).create_payment_requesttakes the amount in VND, a human description, and the criticalidempotency_key.- The key format I recommend:
order_{user_id}_{date}_{attempt_counter}. This prevents collisions across your system. metadatastores agent context—helpful for reconciliation and debugging.- On retry (identical idempotency key), the API returns the cached response instantly. No new charge.
Webhooks: Real-Time Settlement Confirmation
Why Polling Isn't Enough
You could call await_settlement() in a loop every 2 seconds. But that's wasteful: 43,200 API calls per day per active transaction. Webhooks flip the model: the bank notifies you when payment settles.
AgentPay VN's bank feed reads the merchant's actual bank account. When a VietQR payment lands, AgentPay fires a webhook to your endpoint. This is the source of truth—the payment actually cleared.
Setting Up Webhook Handlers
You need an HTTPS endpoint that receives POST requests:
from flask import Flask, request, jsonify
import hmac
import hashlib
import json
from agentpay_vn import verify_webhook_signature
app = Flask(__name__)
WEBHOOK_SECRET = "your_webhook_secret" # Get this from the AgentPay dashboard
@app.route("/webhooks/agentpay", methods=["POST"])
def handle_settlement_webhook():
"""
Receives webhook when a VietQR payment settles.
The bank feed confirms this is real money in your account.
"""
payload = request.get_data(as_text=True)
signature = request.headers.get("X-Signature")
# Verify the webhook came from AgentPay (not a spoofed request)
if not verify_webhook_signature(payload, signature, WEBHOOK_SECRET):
return jsonify({"error": "Invalid signature"}), 403
data = json.loads(payload)
# data now contains:
# {
# "event": "payment.settled",
# "payment_id": "pay_xyz123",
# "idempotency_key": "order_user123_2025_01_15_attempt_1",
# "amount": 500000,
# "status": "settled",
# "timestamp": "2025-01-15T14:32:00Z",
# "bank_ref": "VietQR_abc123"
# }
if data["event"] == "payment.settled":
payment_id = data["payment_id"]
idempotency_key = data["idempotency_key"]
amount = data["amount"]
# Update your database atomically
# Mark enrollment as "PAID"
# Trigger course access provisioning
activate_student_enrollment(idempotency_key, amount)
# Send agent a signal to continue workflow
notify_enrollment_agent(payment_id, status="complete")
return jsonify({"status": "processed"}), 200
return jsonify({"status": "ignored"}), 200
def activate_student_enrollment(idempotency_key, amount):
"""
1. Extract student_id from the idempotency key or your idempotency_key→metadata mapping.
n2. Mark payment as settled in your database.
3. Grant course access immediately.
"""
# Your application logic
pass
if __name__ == "__main__":
# Run on HTTPS (required for webhooks)
app.run(ssl_context="adhoc", port=443, host="0.0.0.0")
Key points:
verify_webhook_signatureprevents spoofing. Always check.- The webhook fires after money settles in your bank. This is the moment to activate access.
idempotency_keyties the webhook back to your original request, even if your database is slow.
Settlement Confirmation: The Full Flow
Creating and Tracking a Payment
Here's a realistic 3-step flow an agent executes:
import asyncio
from agentpay_vn import PaymentClient
import uuid
from datetime import datetime
client = PaymentClient(api_key="your_api_key")
async def agent_sell_course(student_id: str, course_id: str, amount_vnd: int):
"""
An AI agent handles the entire enrollment flow.
This is safe to retry because of idempotency.
"""
# Step 1: Generate a stable idempotency key
timestamp = datetime.utcnow().isoformat()
idempotency_key = f"order_{student_id}_{course_id}_{timestamp}"
print(f"[Agent] Creating payment request for {student_id}...")
# Step 2: Create payment request (idempotent)
payment = client.create_payment_request(
amount=amount_vnd,
description=f"Course enrollment: {course_id}",
idempotency_key=idempotency_key,
metadata={
"student_id": student_id,
"course_id": course_id,
"agent_version": "2.1.0"
}
)
payment_id = payment["id"]
checkout_url = payment["checkout_url"]
print(f"[Agent] Payment created: {payment_id}")
print(f"[Agent] Checkout URL: {checkout_url}")
# Step 3: Wait for settlement (via webhook or polling)
print(f"[Agent] Awaiting settlement...")
settlement = await client.await_settlement(
payment_id=payment_id,
timeout_seconds=3600 # 1 hour timeout
)
if settlement["status"] == "settled":
print(f"[Agent] ✓ Payment settled! Amount: {settlement['amount']} VND")
print(f"[Agent] Bank reference: {settlement['bank_ref']}")
# Trigger course access provisioning
provision_course_access(student_id, course_id)
return {"success": True, "payment_id": payment_id}
else:
print(f"[Agent] ✗ Payment failed: {settlement['status']}")
return {"success": False, "payment_id": payment_id}
def provision_course_access(student_id: str, course_id: str):
"""Unlock the course for the student."""
print(f"[System] Provisioning {course_id} access for {student_id}")
# Your enrollment database logic here
# Agent executes the workflow
result = asyncio.run(agent_sell_course(
student_id="student_5678",
course_id="python_intermediate",
amount_vnd=500000
))
Why this is resilient:
idempotency_keyis deterministic—same inputs generate the same key.- If the agent crashes after Step 2, it can re-run from the top. No duplicate charge.
await_settlementblocks until the bank confirms payment. Then and only then does access get provisioned.
MCP Server for Claude & Other AI Agents
If you're using Claude or another AI agent framework, AgentPay VN provides an MCP (Model Context Protocol) server:
pip install agentpay-vn
agentpay-mcp --api-key your_api_key --webhook-secret your_webhook_secret
Configure Claude with this MCP server in your Claude app's config:
{
"mcpServers": {
"agentpay": {
"command": "agentpay-mcp",
"args": [
"--api-key",
"${AGENTPAY_API_KEY}",
"--webhook-secret",
"${AGENTPAY_WEBHOOK_SECRET}"
]
}
}
}
Now Claude can call:
agentpay_create_payment_request(amount, description, idempotency_key, metadata)agentpay_check_settlement(payment_id)agentpay_list_settlements(limit, offset)
This lets Claude autonomously handle payments in multi-turn conversations:
Claude prompt example:
You are a course enrollment assistant. When a user wants to buy a course:
1. Call agentpay_create_payment_request with amount, description, and a unique idempotency_key.
2. Give the user the checkout_url.
3. Poll agentpay_check_settlement every 5 seconds until settled.
4. Once settled, confirm enrollment and provide access instructions.
Real-World Walkthrough: Online Café Subscription
The Scenario
You run an online specialty coffee shop. Customers can purchase 10-coffee subscription boxes. Your bot handles the entire sales conversation.
The Flow
- Customer messages bot: "I want a subscription box."
- Bot creates payment request:
python idempotency_key = f"coffee_sub_{customer_id}_{uuid.uuid4()}" payment = client.create_payment_request( amount=450000, # 450k VND for 10 specialty coffees description="Premium Coffee Subscription Box (10 packs)", idempotency_key=idempotency_key, metadata={"customer_id": customer_id, "product": "sub_box_v1"} ) - Bot sends checkout URL in the chat.
- Customer scans VietQR with their bank app. Confirms payment.
- Bank processes payment, routes it to your merchant account.
- Webhook fires →
payment.settledevent. - Your system:
- Marks subscription as "ACTIVE"
- Triggers warehouse to pick and ship coffee box
- Sends bot a
settlement_confirmedsignal - Bot replies: "✓ Your subscription is confirmed! Shipping address on file: [address]. You'll receive your coffee within 3 days."
Safety guarantees:
- If the customer reloads the page, the bot re-requests with the same
idempotency_key. Same checkout URL is returned—no duplicate order. - If the bot crashes mid-transaction, it can resume. The payment ID is stored in your database, keyed by
idempotency_key. - The bank feed confirms payment before you ship. No refund surprises.
Do's and Don'ts: Best Practices
| ✅ Do | ❌ Don't |
|---|---|
Use deterministic idempotency_key (include user ID, timestamp, order ID) |
Use random UUIDs for idempotency keys—they defeat the purpose |
Verify webhook signatures with verify_webhook_signature |
Trust unsigned webhooks or skip verification |
| Store idempotency key → payment ID mapping in your database | Rely on agent memory alone; agents can't reliably retain state |
| Wait for settlement webhook before provisioning access | Assume payment succeeded just because checkout_url was generated |
Set reasonable timeout_seconds on await_settlement (300–3600s) |
Wait indefinitely or timeout instantly |
Log bank_ref from settlement for reconciliation |
Discard bank references—you'll need them for audit trails |
| Use HTTPS for webhook endpoints | Expose webhooks over HTTP—payment data in transit must be encrypted |
| Test with small amounts (1,000–10,000 VND) first | Deploy payment logic to production without staging tests |
FAQ: Common Agent Payment Questions
Q: Can an agent call create_payment_request twice by mistake?
A: Yes, but it's harmless. If it uses the same idempotency_key, AgentPay returns the cached response—no new charge. If it uses a different key, a new payment request is created (your checkout URL list grows, but the customer only pays once). Always use the same idempotency key for a logical transaction.
Q: What if the webhook never arrives?
A: await_settlement() has a built-in timeout. After timeout_seconds, it returns the latest known status (polled from the bank). Webhooks are optimizations, not requirements. The bank feed is the source of truth.
Q: How long does settlement take?
A: Typically 5–30 seconds from payment scan to webhook fire. The bank feed queries your merchant account every 30 seconds. So worst-case latency is ~60 seconds. If you need real-time (< 1 second), discuss with AgentPay—some bank integrations offer faster feeds.
Q: Can I refund a settled payment?
A: Not via AgentPay VN (it doesn't hold funds). Money goes straight to your bank. Refunds happen in your bank's portal or via your bank's API. AgentPay tracks what settled; you handle reversals. Always validate bank_ref for refund records.
Key Takeaways
- Idempotency is non-negotiable for agents. Every
create_payment_requestmust include a stable, uniqueidempotency_key. On retry, the same key returns the same response—zero side effects. - Webhooks > polling. Listen to bank feed webhooks for settlement confirmation. It's faster, cheaper, and more reliable than polling. Always verify signatures.
- Money flows directly to your bank. AgentPay VN is a QR-generation and bank-feed reader, not a money processor. This simplifies compliance and reduces your fraud surface.
- Settlement is your gate. Don't provision access, ship products, or mark orders "paid" until the settlement webhook fires. The bank feed is the single source of truth.
- Test idempotency in staging. Deliberately retry requests. Verify you get the same response, not duplicate charges. This confidence pays dividends in production.
- Log everything. Store idempotency keys, payment IDs, bank references, and webhook timestamps. Reconciliation and debugging depend on it.
Getting Started
-
Install AgentPay VN:
bash pip install agentpay-vn -
Read the full docs: https://agentpay.servicesai.vn/v1/docs
-
Explore the GitHub repo for examples: https://github.com/phuocdu/agentpay-vn
-
Deploy the MCP server if you're using Claude or another agent framework.
-
Start with a test payment (1,000–10,000 VND). Verify idempotency by retrying with the same key. Confirm webhook delivery. Then go live.
Your AI agents can now collect payments safely, idempotently, and with full settlement visibility. Build with confidence.