Agent-native payments: idempotency, webhooks & settlement
The Problem: Why AI Agents Fail at Taking Money
You've built a brilliant AI agent—it understands customer intent, negotiates terms, and closes deals. But the moment it needs to collect payment, everything falls apart.
A customer asks your agent to process a $50 course enrollment. The agent creates a payment request, sends the checkout link... then your LLM call times out. Did the payment go through? Should the agent retry? If it does, will you charge the customer twice? Now your agent is stuck in a loop, the customer is confused, and your bank feed shows duplicate transactions.
This is the idempotency crisis—and it's why most teams still route payments through manual dashboards instead of letting agents handle them end-to-end.
AgentPay VN solves this by design. Built for AI agents, it guarantees that no matter how many times your agent retries a payment request, you'll never double-charge. Combined with webhooks and settlement confirmation, you get a payment flow that's reliable enough for production systems handling real money.
Let's build it.
Understanding Idempotency in Agent Payments
Why It Matters for AI Systems
Unlike humans, AI agents retry constantly. Network hiccups, token limits, tool timeouts—they all trigger retries. A traditional payment API without idempotency protection means chaos:
- Scenario 1: Agent creates payment request, gets timeout, retries. Now you have two payment requests for one order.
- Scenario 2: Customer balance deducted twice. Support tickets explode.
- Scenario 3: Agent can't determine if a payment succeeded, so it keeps trying. Settlement confirmation never arrives.
Idempotency is the answer. AgentPay VN uses idempotency keys—unique identifiers you provide for each payment request. If your agent retries with the same key, AgentPay returns the cached result instead of creating a new request. Same key, same response, every time.
How AgentPay VN Implements It
Every payment request includes an idempotency_key parameter. This key is typically a deterministic hash of:
- Order ID
- Customer ID
- Amount
- Merchant ID
If your agent retries 10 times with the same key, AgentPay VN will return the same VietQR checkout URL. No duplicate charges. No confusion.
from agentpay_vn import AgentPay
# Initialize the SDK
agent_pay = AgentPay(api_key="your_api_key_here")
# Create a payment request with idempotency
response = agent_pay.create_payment_request(
amount=500000, # 500,000 VND
description="Online Course: Python Mastery",
merchant_id="merchant_12345",
idempotency_key="order_2024_001_customer_789", # Unique identifier
metadata={"course_id": "python_101", "student_email": "alice@example.com"}
)
print(f"Checkout URL: {response['checkout_url']}")
print(f"Payment Request ID: {response['id']}")
# Agent retries with the SAME idempotency_key
# AgentPay returns the cached response—no duplicate request
retry_response = agent_pay.create_payment_request(
amount=500000,
description="Online Course: Python Mastery",
merchant_id="merchant_12345",
idempotency_key="order_2024_001_customer_789", # Same key = same result
metadata={"course_id": "python_101", "student_email": "alice@example.com"}
)
assert response['id'] == retry_response['id'] # Same payment request
assert response['checkout_url'] == retry_response['checkout_url'] # Same link
Line-by-line breakdown:
- Lines 1–3: Import and initialize AgentPay with your API key (get from dashboard).
- Lines 6–12: Create a payment request with a unique idempotency_key. This key should be deterministic—same input always generates the same key.
- Lines 14–15: Log the checkout URL your agent will send to the customer.
- Lines 18–24: Retry with the same idempotency key. AgentPay detects the duplicate and returns the cached result.
- Lines 26–27: Assertions verify that retrying with the same key returns the exact same payment request and URL.
Webhooks: Real-Time Settlement Confirmation
Why Polling Isn't Good Enough
Your agent creates a payment request and sends the VietQR link to the customer. Now what? Poll await_settlement() every 5 seconds? That's wasteful, slow, and fragile.
Webhooks flip the script. When a customer completes payment, AgentPay VN sends an HTTP POST to your endpoint with settlement details. No polling. No delays. Real-time confirmation that money landed in the merchant's bank account.
Setting Up Webhooks
- Configure your endpoint in the AgentPay VN dashboard (e.g.,
https://your-api.com/webhooks/agentpay). - Verify the signature using the shared secret (prevents spoofing).
- Process the settlement event and notify your agent.
Here's a minimal webhook handler:
from flask import Flask, request
import hmac
import hashlib
import json
app = Flask(__name__)
WEBHOOK_SECRET = "your_webhook_secret_from_dashboard"
@app.route('/webhooks/agentpay', methods=['POST'])
def handle_settlement_webhook():
# Verify webhook signature
signature = request.headers.get('X-AgentPay-Signature')
payload = request.get_data(as_text=True)
expected_signature = hmac.new(
WEBHOOK_SECRET.encode(),
payload.encode(),
hashlib.sha256
).hexdigest()
if signature != expected_signature:
return {"error": "Invalid signature"}, 401
# Parse event
event = json.loads(payload)
if event['type'] == 'settlement.confirmed':
payment_id = event['payment_id']
amount = event['amount']
merchant_account = event['merchant_account']
# Update your database
print(f"✓ Settlement confirmed: {payment_id}, {amount} VND to {merchant_account}")
# Notify your agent (via message queue, database, etc.)
# agent.process_settlement(payment_id)
return {"status": "received"}, 200
return {"status": "ignored"}, 200
if __name__ == '__main__':
app.run(port=5000)
Key points:
- Signature verification (lines 11–20): Prevents fake webhooks. AgentPay signs every webhook; verify it matches.
- Event type check (line 24): AgentPay sends multiple event types; you handle settlement.confirmed.
- Database update (line 29): Mark the order as paid in your system.
- Agent notification (line 32): Queue a message or update a shared state so your agent knows the customer's payment succeeded.
The Full Agent Payment Flow
Step 1: Create Payment Request (with Idempotency)
Your agent receives a customer's payment intent. It generates a deterministic idempotency key and creates a payment request:
from agentpay_vn import AgentPay
import hashlib
agent_pay = AgentPay(api_key="sk_live_...")
def process_agent_payment(order_id, customer_id, amount, description):
# Generate deterministic idempotency key
key_source = f"{order_id}_{customer_id}_{amount}"
idempotency_key = hashlib.sha256(key_source.encode()).hexdigest()[:32]
# Create payment request
response = agent_pay.create_payment_request(
amount=int(amount * 100), # Convert to smallest unit (xu)
description=description,
merchant_id="merchant_12345",
idempotency_key=idempotency_key,
metadata={"order_id": order_id, "customer_id": customer_id}
)
return response['checkout_url']
# Agent calls this
checkout_url = process_agent_payment(
order_id="ORD_2024_001",
customer_id="CUST_789",
amount=500000, # 500,000 VND
description="Premium Course Bundle"
)
print(f"Send this link to customer: {checkout_url}")
Step 2: Send Checkout URL
Your agent sends the VietQR checkout URL to the customer via email, SMS, or chat:
"Hi Alice! Here's your VietQR payment link:
https://vietqr.io/checkout/agentpay_vn_...
Scan or tap to pay 500,000 VND for the course. It's instant!"
Step 3: Await Settlement
Your agent (or webhook handler) waits for settlement confirmation:
import time
def wait_for_settlement(payment_id, timeout_seconds=300):
"""Poll for settlement confirmation (or use webhooks for production)."""
start = time.time()
while time.time() - start < timeout_seconds:
status = agent_pay.await_settlement(
payment_id=payment_id,
timeout=5 # Check every 5 seconds
)
if status['settled']:
print(f"✓ Payment settled! Merchant account: {status['merchant_account']}")
return True
time.sleep(2) # Brief pause before retry
print("⚠ Payment not settled within timeout.")
return False
# Usage
settled = wait_for_settlement(payment_id="pay_abc123")
if settled:
# Enroll student, unlock content, etc.
enroll_student(customer_id="CUST_789", course_id="python_101")
else:
# Send reminder email
send_payment_reminder(email="alice@example.com")
Note: For production, prefer webhooks over polling. Webhooks are real-time and eliminate the need for timeouts.
Real-World Walkthrough: Course-Selling Bot
Let's trace a complete scenario: an AI bot that sells online courses.
Setup:
- Bot platform: Claude + MCP server (AgentPay VN MCP)
- Course platform: Your API
- Payment processor: AgentPay VN
- Webhook endpoint: https://coursebot.ai/webhooks/payment
The Flow:
- Customer: "I want to buy the Python course for 500,000 VND."
- Bot (via MCP tool): Calls
create_payment_requestwith idempotency keycrs_course_python_cust_alice. - AgentPay VN: Returns checkout URL
https://vietqr.io/checkout/req_xyz... - Bot: "Great! Scan this QR code or click here to pay."
- Customer: Scans QR, bank app opens, they tap "Confirm Payment" (2–3 seconds).
- Bank: Processes transfer to merchant's VietComBank account instantly.
- AgentPay VN: Detects settlement in bank feed, sends webhook:
{"type": "settlement.confirmed", "payment_id": "pay_xyz", "amount": 500000} - Your webhook handler: Verifies signature, updates database (
orders.paid = true), queues a message. - Bot (receiving update): "Congratulations! You're enrolled. Here's your login: [link]."
Total time: ~5 seconds from scan to course access. No redirect loops. No checkout pages. Just QR → payment → confirmation.
MCP Server Configuration for Claude
To integrate AgentPay VN with Claude via MCP:
{
"mcpServers": {
"agentpay-vn": {
"command": "npx",
"args": ["agentpay-mcp"],
"env": {
"AGENTPAY_API_KEY": "sk_live_your_key_here",
"AGENTPAY_MERCHANT_ID": "merchant_12345",
"AGENTPAY_WEBHOOK_SECRET": "whsec_..."
}
}
}
}
Once configured, Claude gains access to three tools:
- create_payment_request(amount, description, idempotency_key, metadata)
- await_settlement(payment_id, timeout)
- check_payment_status(payment_id)
Claude uses these tools in conversation, keeping the context of your payment flow.
Do's and Don'ts
| ✓ Do | ✗ Don't |
|---|---|
| Use deterministic idempotency keys (hash of order + customer + amount) | Generate random UUIDs for idempotency keys—they won't deduplicate on retry |
| Verify webhook signatures with the shared secret | Trust webhook payloads without verification |
| Store payment_id and idempotency_key in your database | Lose track of which payments your agent created |
| Implement exponential backoff if polling (5s, 10s, 20s...) | Hammer the API with polling every 100ms |
| Test idempotency by retrying the same request 5+ times | Assume it works without testing |
| Log all payment events (request, settlement, webhook received) | Skip logging—you won't debug issues later |
Advanced Tips
1. Metadata for Context Recovery
Store extra data in the metadata field. If something goes wrong, you can recover context:
metadata={
"order_id": "ORD_2024_001",
"customer_id": "CUST_789",
"customer_email": "alice@example.com",
"course_id": "python_101",
"agent_session_id": "sess_abc123",
"retry_count": 0
}
When the webhook fires, you have all the context to fulfill the order correctly.
2. Idempotency Window
AgentPay VN caches idempotency results for 24 hours. After that, the same key creates a new payment request. This is intentional—it prevents accidental recharges of old orders if your agent somehow retries after a full day.
3. Testing Webhooks Locally
Use ngrok to expose your localhost webhook endpoint:
ngrok http 5000
Then update your webhook URL in the AgentPay dashboard to https://your-ngrok-url.ngrok.io/webhooks/agentpay. Webhooks will now route to your local Flask server.
4. Concurrent Payments
If your agent handles multiple customers simultaneously, use a queue (Redis, Celery) to serialize settlement processing. Webhooks can arrive out of order; a queue ensures idempotent updates:
# Celery task
@app.task(bind=True, max_retries=3)
def process_settlement(self, payment_id, amount):
try:
# Idempotent database operation
order = Order.objects.get(payment_id=payment_id)
if order.status != 'paid':
order.status = 'paid'
order.save()
enroll_customer(order.customer_id, order.course_id)
except Exception as exc:
# Retry with exponential backoff
raise self.retry(exc=exc, countdown=2 ** self.request.retries)
FAQ
Q: What if a customer scans the QR but then closes their banking app without completing the transfer?
A: No settlement webhook is sent. Your agent's timeout expires (default 5 minutes), and it can send a reminder email or prompt the customer again. The payment request remains active for 24 hours—the customer can retry anytime.
Q: Can my agent create multiple payment requests for the same order?
A: Yes, but only if you use different idempotency keys. This is useful if a customer wants to split a payment across two transactions. Just ensure your backend logic handles partial payments correctly.
Q: Does AgentPay VN hold money between the bank and my merchant account?
A: No. AgentPay VN is a payment orchestrator, not a money intermediary. The VietQR points directly at your merchant's bank account. When a customer transfers funds, it goes straight to your bank—no AgentPay wallet, no holding period. Settlement is confirmed when your bank's feed reflects the deposit.
Q: What's the difference between await_settlement() and webhooks?
A: await_settlement() is for testing or simple flows—you poll until the payment settles (blocks your agent). Webhooks are for production—AgentPay pushes settlement events to your server in real-time, freeing your agent to handle other tasks.
Key Takeaways
- Idempotency keys prevent double-charging: Use deterministic hashes of order + customer + amount. Retry with the same key, get the same result.
- Webhooks enable real-time settlement: No polling, no delays. AgentPay pushes confirmation to your endpoint; verify the signature and process immediately.
- AgentPay VN never holds money: QR points straight to merchant account. Bank feed confirms settlement. You control the flow end-to-end.
- Metadata is your recovery tool: Store order IDs, customer info, and agent context in payment metadata. Use it to fulfill orders correctly when webhooks arrive.
- Test idempotency aggressively: Retry the same request 5+ times in staging. Verify you get identical responses and no duplicate charges.
- Log everything: Payment requests, settlement events, webhook receipts. Future debugging depends on it.
Get Started Today
Ready to build agent-native payments? Here's your path:
- Install the SDK:
pip install agentpay-vn - Read the docs: https://agentpay.servicesai.vn/v1/docs
- Check the GitHub repo: https://github.com/phuocdu/agentpay-vn (MIT license, open-source)
- Deploy the MCP server:
agentpay-mcpfor Claude integration - Test locally: Use ngrok to expose your webhook endpoint and trace a full payment flow
Your AI agents are ready to collect real money, reliably. No more manual dashboards. No more double-charges. Just frictionless, idempotent payments that scale.
Let's build it.