Agent-Native Payments: Idempotency, Webhooks & Settlement
The Problem: AI Agents Handling Money Without Breaking
Imagine you've built an AI sales agent that closes deals—it converses, answers objections, and at checkout says: "Please scan this QR code." The customer scans. Great. But then your agent crashes mid-request, or the network hiccups, and you retry. Now you've created two payment requests. The customer sees confusion. Your merchant loses trust. Money flows to the wrong place or duplicates in your logs.
This is the core friction when AI agents handle real payments. Traditional web apps have sessions and CSRF tokens. Agents? They're stateless, distributed, and often retry without thinking. Add VietQR—Vietnam's real-time bank transfer standard—and you need bulletproof idempotency, reliable webhooks, and crystal-clear settlement confirmation.
AgentPay VN solves this by embedding payment logic directly into your agent's SDK, with built-in idempotency keys, webhook verification, and settlement await. The QR points straight to the merchant's bank; AgentPay never touches the money. Let's build it right.
Understanding the AgentPay VN Architecture
Before we code, let's map the flow:
- Agent calls
create_payment_request()with a unique idempotency key → generates a VietQR QR code. - Agent sends checkout URL to the customer (via email, SMS, or chat).
- Customer scans QR, pays via their bank app → money goes straight to merchant's account.
- Bank webhook notifies AgentPay → AgentPay verifies the signature and triggers your settlement handler.
- Agent calls
await_settlement()to block until payment confirms → or poll periodically.
The critical insight: AgentPay is a bridge, not a wallet. It never holds funds. It verifies transfers and confirms settlements. Your agent retains full control and transparency.
Idempotency: The Agent's Shield Against Retries
Idempotency is non-negotiable. If your agent retries a create_payment_request() call (due to timeout, network glitch, or orchestration logic), you must get the same payment request back, not a duplicate.
AgentPay VN implements idempotency via a request_id parameter—a UUID your agent generates once per payment and reuses:
import uuid
from agentpay_vn import PaymentClient
# Initialize the client
client = PaymentClient(api_key="your_api_key")
# Generate a unique request ID (do this ONCE per payment flow)
request_id = str(uuid.uuid4())
merchant_account = "0123456789" # Merchant's bank account
amount_vnd = 500000 # 500k VND
# First call: creates the request
payment_req = client.create_payment_request(
request_id=request_id,
merchant_account=merchant_account,
amount=amount_vnd,
description="Course: Advanced Python Patterns"
)
print(f"QR Code URL: {payment_req['checkout_url']}")
print(f"Request ID (save this): {payment_req['request_id']}")
# Network fails here. Agent retries...
# Second call with SAME request_id: returns the SAME payment_req, no duplicate
payment_req_retry = client.create_payment_request(
request_id=request_id,
merchant_account=merchant_account,
amount=amount_vnd,
description="Course: Advanced Python Patterns"
)
assert payment_req_retry['request_id'] == payment_req['request_id']
print("✓ Idempotency verified: same request ID, same response")
Line-by-line:
- uuid.uuid4(): generates a 36-char unique identifier—store this in your agent's memory/database.
- create_payment_request(): accepts the request_id; AgentPay's backend deduplicates. If it sees the same request_id again within 24 hours, it returns the original request without creating a new one.
- The merchant_account, amount, and description should remain identical on retries—AgentPay validates consistency.
Pro tip: Bind the request_id to the user session, order ID, or conversation thread. This makes debugging audits trivial.
Webhooks: Confirming Settlements in Real-Time
After the customer pays, their bank notifies AgentPay via webhook. AgentPay then notifies your agent via a signed webhook POST. This is where settlement confirmation happens.
Webhooks are essential because: - No polling overhead: You don't spam AgentPay asking "Is it settled yet?" - Real-time reaction: Your agent can immediately issue a digital product, send a receipt, or trigger downstream workflows. - Security: AgentPay signs each webhook with HMAC-SHA256; you verify before trusting.
Setting Up a Webhook Handler
In your agent's code (or a companion microservice), define a webhook endpoint:
from flask import Flask, request, jsonify
import hmac
import hashlib
import json
app = Flask(__name__)
WEBHOOK_SECRET = "your_webhook_secret_from_agentpay_dashboard"
def verify_webhook_signature(payload_str, signature):
"""Verify HMAC-SHA256 signature from AgentPay."""
expected_sig = hmac.new(
WEBHOOK_SECRET.encode(),
payload_str.encode(),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected_sig, signature)
@app.route('/webhooks/agentpay-settlement', methods=['POST'])
def handle_settlement_webhook():
"""Receive and process payment settlement confirmation."""
payload = request.get_json()
signature = request.headers.get('X-AgentPay-Signature')
payload_str = json.dumps(payload, separators=(',', ':'), sort_keys=True)
# Verify signature
if not verify_webhook_signature(payload_str, signature):
print("⚠ Invalid signature—reject webhook")
return jsonify({"error": "Unauthorized"}), 401
# Extract settlement data
event_type = payload.get('event')
request_id = payload.get('request_id')
amount = payload.get('amount')
merchant_account = payload.get('merchant_account')
bank_ref = payload.get('bank_ref_number') # Bank's transaction ID
settled_at = payload.get('settled_at') # ISO 8601 timestamp
if event_type == 'settlement.confirmed':
print(f"✓ Payment settled: {request_id}, {amount} VND, bank ref: {bank_ref}")
# Trigger your agent's settlement handler
# E.g., unlock digital product, send receipt, update order status
handle_agent_settlement(request_id, amount, bank_ref, settled_at)
return jsonify({"status": "received"}), 200
elif event_type == 'settlement.failed':
print(f"✗ Payment failed: {request_id}")
handle_agent_failure(request_id)
return jsonify({"status": "received"}), 200
return jsonify({"error": "Unknown event"}), 400
def handle_agent_settlement(request_id, amount, bank_ref, settled_at):
"""Your custom logic: unlock content, send email, etc."""
print(f"Processing settlement for request {request_id}...")
# Store settlement record
# Trigger downstream agents (e.g., email bot, CRM update)
def handle_agent_failure(request_id):
"""Handle failed payments (e.g., refund request, retry prompt)."""
print(f"Processing failure for request {request_id}...")
if __name__ == '__main__':
app.run(port=5000, debug=False)
Key points:
- X-AgentPay-Signature header contains the HMAC-SHA256 hash of the JSON payload.
- Always verify the signature before processing—this confirms the webhook is from AgentPay, not an attacker.
- Store the bank_ref_number—this is the authoritative identifier for the settlement in the merchant's bank.
- Idempotent webhook handling: if you receive the same webhook twice (network retry), process it only once. Use request_id + bank_ref_number as a composite key to detect duplicates.
Integrating with Claude via MCP Server
If you're using Claude as your agent, AgentPay provides an MCP (Model Context Protocol) server that exposes payment functions natively:
{
"mcpServers": {
"agentpay": {
"command": "agentpay-mcp",
"env": {
"AGENTPAY_API_KEY": "your_api_key",
"AGENTPAY_WEBHOOK_SECRET": "your_webhook_secret"
}
}
}
}
Place this in your Claude/MCP configuration file. Now Claude can call:
- agentpay_create_payment_request: generates VietQR QR.
- agentpay_await_settlement: blocks until payment settles (30-second timeout).
- agentpay_get_status: checks payment status by request_id.
Claude's tools will automatically validate request_ids, amounts, and merchant accounts—no manual validation needed.
Real-World Walkthrough: Online Course Bot
Let's trace a concrete flow: a customer (An) buys an online Python course via ChatBot (powered by Claude + AgentPay VN).
Step 1: Conversation
- An: "I want to buy your Advanced Python course. Cost?"
- ChatBot: "That's 500,000 VND. I'll create a payment link for you."
- ChatBot calls create_payment_request() with a new UUID request_id, merchant's bank account, and amount.
Step 2: Checkout - ChatBot sends: "Scan this QR code: https://checkout.agentpay.vn/qr?id=abc123" - An scans with her bank app (VietQR-compatible). - Her bank displays: "Transfer 500,000 VND to [Merchant Name]? YES/NO." - She confirms → transfer sent immediately to the merchant's bank account.
Step 3: Settlement Notification
- Merchant's bank (e.g., Vietcombank) notifies AgentPay's webhook: "Transfer received, ref #VCB123456."
- AgentPay verifies the transfer (amount, recipient, timestamp).
- AgentPay calls your webhook endpoint with event: 'settlement.confirmed', bank_ref_number: 'VCB123456'.
Step 4: Agent Reacts - Your webhook handler receives the confirmation, verifies the signature. - Your app marks the order as paid, generates a course access token. - Your app triggers an automated email: "Welcome! Your course is ready at [link]. Access code: [code]." - (Optional) Your ChatBot notices the settlement via polling or pub/sub and tells An: "Payment received! Check your email for the course link."
Why no confusion? Idempotency keys. If the ChatBot retries create_payment_request() (network glitch), it sends the same UUID, so AgentPay returns the same QR code. An doesn't see two links. The webhook handler uses bank_ref_number as an idempotency key for settlement logic—even if the webhook is delivered twice, your app processes it once.
Advanced Tips: Polling, Timeouts, and Error Handling
While webhooks are preferred, sometimes you need to poll (e.g., your webhook endpoint is unreachable, or you want a synchronous agent flow):
import time
from agentpay_vn import PaymentClient
client = PaymentClient(api_key="your_api_key")
request_id = "550e8400-e29b-41d4-a716-446655440000"
# Option 1: Block until settlement (blocking await_settlement call)
print("Waiting for settlement...")
try:
settlement = client.await_settlement(
request_id=request_id,
timeout_seconds=120 # Wait up to 2 minutes
)
print(f"✓ Settled! Bank ref: {settlement['bank_ref_number']}, amount: {settlement['amount']}")
except TimeoutError:
print("✗ Settlement not received within 2 minutes. Customer may still be paying.")
except Exception as e:
print(f"✗ Error: {e}")
# Option 2: Poll with custom interval
print("\nPolling for settlement status...")
for attempt in range(60): # Try 60 times (10 minutes at 10-second intervals)
status = client.get_payment_status(request_id=request_id)
print(f"Attempt {attempt + 1}: status = {status['state']}")
if status['state'] == 'settled':
print(f"✓ Settled! Bank ref: {status['bank_ref_number']}")
break
elif status['state'] == 'failed':
print("✗ Payment failed.")
break
elif status['state'] == 'expired':
print("✗ QR code expired (typically 24 hours).")
break
time.sleep(10) # Wait 10 seconds before next check
else:
print("✗ Polling timed out.")
When to use each: - await_settlement(): Synchronous agent flows where you want to wait inline (e.g., "Process payment, then unlock."). - Webhooks: Asynchronous, production-grade; low latency, no polling overhead. - Polling: Fallback for webhook reliability issues; good for batch reconciliation (e.g., daily settlement reports).
Do's and Don'ts
| Do | Don't |
|---|---|
| ✓ Generate a new UUID per payment request | ✗ Reuse the same request_id across different customers/payments |
| ✓ Verify webhook signatures with HMAC-SHA256 | ✗ Trust webhook payload without verification |
✓ Store bank_ref_number as the source of truth |
✗ Rely only on your internal payment IDs |
| ✓ Implement idempotent webhook handlers (check request_id + bank_ref) | ✗ Process webhooks without checking for duplicates |
| ✓ Set merchant_account to the vendor's actual bank account | ✗ Hardcode placeholder or test accounts in production |
✓ Use await_settlement() with appropriate timeout |
✗ Block indefinitely or use zero timeout |
| ✓ Log all webhook events for audit trails | ✗ Silently discard failed webhook signatures |
| ✓ Handle network glitches gracefully | ✗ Assume first request always succeeds |
Installation and Setup
Install the SDK:
pip install agentpay-vn
Install the MCP server (for Claude/LLM integration):
pip install agentpay-mcp
Get your API key: 1. Visit AgentPay VN Dashboard. 2. Sign up or log in. 3. Create a merchant account and link your bank. 4. Generate an API key and a webhook secret. 5. Store them securely (environment variables, secrets manager).
FAQ
Q: Does AgentPay hold my money? A: No. AgentPay is a bridge only. The VietQR points directly to your merchant bank account. Money settles in your bank within seconds, not hours. AgentPay never touches funds.
Q: What if a webhook is delivered twice?
A: This can happen due to network retries. Always use request_id + bank_ref_number as a composite key to detect and skip duplicate events. Store a settlement record after first processing.
Q: Can I use AgentPay VN outside Vietnam? A: AgentPay is designed for VietQR, which operates within Vietnam. International customers would need a Vietnamese bank account. For global payments, consider other solutions.
Q: How long does settlement take? A: VietQR transfers settle almost instantly (within seconds). Your bank may take 1-2 minutes to push the webhook. Budget 30-60 seconds from payment scan to settlement confirmation in your agent.
Q: What if the customer scans the QR but the payment fails?
A: Your webhook will receive event: 'settlement.failed'. Common causes: insufficient funds, wrong PIN, or timeout at the bank. Notify the customer and either retry or offer an alternative payment method.
Key Takeaways
- Idempotency is mandatory: Use UUIDs as request_ids to prevent duplicate payments if your agent retries.
- Webhooks are the gold standard: Implement signature verification (HMAC-SHA256) and idempotent handlers. Avoid polling unless necessary.
- Bank reference numbers are authoritative: Store
bank_ref_numberfrom settlement webhooks as your source of truth, not internal IDs. - AgentPay never holds money: Funds flow directly to your merchant bank account. AgentPay confirms the transfer and notifies your agent.
- VietQR is fast: Expect settlement within seconds, not hours. Your agent can react in near-real-time.
- MCP integration with Claude: Use the agentpay-mcp server to expose payment functions as Claude tools—no manual parsing needed.
- Test thoroughly: Use the sandbox/test mode to verify idempotency, webhook handling, and error scenarios before going live.
Next Steps
You're ready to integrate AI-native payments. Start here:
- Install the SDK:
pip install agentpay-vn - Read the full docs: AgentPay VN Documentation
- Explore the source: GitHub: phuocdu/agentpay-vn
- Set up your merchant account and generate an API key.
- Clone the example agent (course bot, café checkout, online shop) from the repo and adapt it.
- Deploy your webhook endpoint and test with sandbox mode.
- Go live and let your agents handle payments with confidence.
Questions? The community is active on the AgentPay GitHub discussions. Happy building! 🚀