Agent-Native Payments: Idempotency, Webhooks & Settlement
The Problem: When Your AI Agent Forgets It Already Charged
Imagine this: Your AI chatbot is selling online courses. A customer clicks "Buy Now," the agent creates a payment request, sends the checkout URL—but the API hiccups mid-response. The HTTP connection breaks. Your agent, unsure if the request went through, retries. Now two payment requests exist for the same course. The customer sees duplicate QR codes. Chaos.
Or worse: Your agent collects the payment webhook, records the settlement, then crashes before confirming to the customer. No settlement confirmation sent. The money landed in the merchant account, but the buyer thinks payment failed and tries again.
This is the idempotency and settlement visibility problem—and it's why production payment flows fail. AgentPay VN solves this by building idempotency and webhook-driven settlement into the SDK itself. This tutorial shows you how to use it.
What Makes Agent-Native Payments Different
Traditional payment APIs assume a human user clicks once and waits. Agent-native payment APIs must handle:
- Retry logic without duplicates — Your agent might retry a request multiple times; you need one payment, not five.
- Async settlement — The payment doesn't settle instantly. Your agent must wait for a webhook or poll for confirmation.
- State without side effects — Your agent runs in isolated invocations; payment state lives in a backend, not in memory.
AgentPay VN's design reflects this:
- VietQR QR codes point directly to the merchant's bank account (never AgentPay's). This means zero trust friction: the customer can verify the account name before paying.
- Bank feed settlement: When money lands in the merchant account, a webhook fires to confirm it.
- Idempotent request IDs: Create a payment with a deterministic ID; retry forever; you always get one payment.
Idempotency: The Foundation of Reliability
Idempotency means calling an operation 100 times has the same effect as calling it once.
In AgentPay VN, every create_payment_request() call includes an idempotency key. If your agent retries with the same key, the server returns the same payment request without creating a duplicate.
How to Design Idempotent Requests
The idempotency key must be deterministic—derived from the payment's semantic identity, not a random UUID.
import hashlib
from agentpay_vn import create_payment_request
# Bad: random key (creates duplicates on retry)
idempotency_key = str(uuid4())
# Good: deterministic key based on customer + order
order_id = "order_12345"
customer_id = "user_789"
amount_vnd = 299000
# Create a stable hash from immutable order details
idempotency_key = hashlib.sha256(
f"{customer_id}:{order_id}:{amount_vnd}".encode()
).hexdigest()[:32]
# Create payment request with idempotency key
payment = create_payment_request(
amount=amount_vnd,
description="Online Course: Python Mastery",
idempotency_key=idempotency_key, # Deterministic, safe to retry
)
print(f"Payment ID: {payment.id}")
print(f"Checkout URL: {payment.checkout_url}")
print(f"Status: {payment.status}")
Line-by-line explanation:
- Line 8–12: We hash the customer ID, order ID, and amount. This ensures the same order always produces the same key.
- Line 14–18: We pass this key to create_payment_request(). If the agent crashes and retries with the same key, AgentPay returns the existing payment—no duplicate.
- Line 20–23: The response includes the payment ID and a checkout URL your agent sends to the customer.
When to Retry
Retry with the same idempotency key if: - Network timeout (no response after 5 seconds). - 5xx server error (500, 502, 503, 504). - Connection reset.
Do NOT retry on 4xx errors (400, 401, 422)—these indicate a problem with your request, not transience.
Webhooks: The Heartbeat of Settlement
After the customer pays via VietQR, the money lands in the merchant's bank account. AgentPay VN watches the bank feed and fires a webhook to notify your agent.
Setting Up Webhook Listeners
Webhooks are HTTP POST requests sent by AgentPay to a URL you control. Your agent must:
- Register a webhook endpoint in AgentPay VN config.
- Verify the webhook signature (never trust unsigned requests).
- Process the settlement event idempotently (in case the same webhook fires twice).
- Acknowledge receipt (return 200 OK within 5 seconds).
Here's an example webhook receiver:
import hmac
import hashlib
import json
from flask import Flask, request
from agentpay_vn import verify_webhook_signature
app = Flask(__name__)
WEBHOOK_SECRET = "your_webhook_secret_from_dashboard"
@app.route("/webhooks/agentpay", methods=["POST"])
def handle_settlement_webhook():
"""
Receives settlement confirmation from AgentPay VN.
Bank feed detected payment; money is in our account.
"""
payload = request.get_json()
signature = request.headers.get("X-AgentPay-Signature")
# Verify signature to confirm this came from AgentPay
if not verify_webhook_signature(
payload=json.dumps(payload, sort_keys=True),
signature=signature,
secret=WEBHOOK_SECRET
):
return {"error": "Invalid signature"}, 401
# Extract event details
event_type = payload.get("event_type")
payment_id = payload.get("payment_id")
amount = payload.get("amount")
settlement_time = payload.get("settled_at")
# Handle settlement event idempotently
if event_type == "payment.settled":
# Check if we already processed this settlement
existing = db.query(Settlement).filter(
Settlement.payment_id == payment_id
).first()
if existing:
# Already recorded; just acknowledge
return {"status": "already_processed"}, 200
# Record settlement for the first time
settlement = Settlement(
payment_id=payment_id,
amount=amount,
settled_at=settlement_time,
status="confirmed"
)
db.add(settlement)
db.commit()
# Notify the customer
customer_order = db.query(Order).filter(
Order.payment_id == payment_id
).first()
if customer_order:
send_course_access_email(customer_order.customer_id)
# Must respond within 5 seconds
return {"status": "received"}, 200
if __name__ == "__main__":
app.run(port=8000)
Key points: - Line 11–19: Verify the webhook signature using AgentPay's secret. This proves the request came from AgentPay, not a hacker. - Line 25–31: Extract payment and settlement details. - Line 33–43: Check if we already recorded this settlement. If yes, return 200 without re-processing. This handles the case where AgentPay retries the webhook. - Line 45–56: Record the settlement and notify the customer (e.g., email access to a course). - Line 58–59: Return 200 OK quickly. If AgentPay doesn't get 200 within 5 seconds, it will retry.
Webhook Retry Logic
AgentPay VN retries failed webhooks with exponential backoff:
- Attempt 1: Immediately
- Attempt 2: 5 minutes later
- Attempt 3: 30 minutes later
- Attempt 4: 2 hours later
- Attempt 5: 24 hours later
If all 5 attempts fail, the webhook is marked dead. Check the AgentPay dashboard for failed webhooks.
The Complete Payment Flow: 3-Line Design
AgentPay VN's core flow is intentionally simple:
from agentpay_vn import create_payment_request, await_settlement
import hashlib
# Step 1: Create a payment request (idempotent)
order_data = {
"customer_id": "user_456",
"order_id": "order_shop_abc_001",
"amount": 499000, # VND
"description": "Premium online course"
}
idempotency_key = hashlib.sha256(
f"{order_data['customer_id']}:{order_data['order_id']}".encode()
).hexdigest()[:32]
payment = create_payment_request(
amount=order_data["amount"],
description=order_data["description"],
idempotency_key=idempotency_key
)
print(f"VietQR Checkout URL: {payment.checkout_url}")
# Agent sends this URL to customer via chat/email/SMS
# Step 2: Await settlement (via webhook or polling)
settlement = await_settlement(
payment_id=payment.id,
timeout_seconds=600 # Wait up to 10 minutes
)
if settlement.status == "confirmed":
print(f"Payment settled! Amount: {settlement.amount} VND")
# Grant course access, send confirmation, etc.
else:
print(f"Settlement pending or failed: {settlement.status}")
The flow in plain English: 1. Create: Agent builds a payment request with a stable idempotency key. 2. Send: Agent gives the customer a checkout URL (VietQR QR code). 3. Settle: Agent waits for bank confirmation via webhook or polling.
Real-World Walkthrough: Café Loyalty Chatbot
You're building a Telegram chatbot for a café that sells loyalty cards. Customers message "Buy 10-punch card for 99,000 VND" and the bot should:
- Create a payment request.
- Send a VietQR QR code.
- Wait for payment confirmation.
- Issue a loyalty card ID.
from agentpay_vn import create_payment_request, await_settlement
import hashlib
import uuid
from telegram import Update
from telegram.ext import ContextTypes
async def handle_buy_card(update: Update, context: ContextTypes.DEFAULT_TYPE):
user_id = str(update.message.from_user.id)
card_sku = "card_10punch"
amount_vnd = 99000
# Idempotent key: customer + product + timestamp (per day)
from datetime import datetime
today = datetime.now().strftime("%Y-%m-%d")
idempotency_key = hashlib.sha256(
f"{user_id}:{card_sku}:{today}".encode()
).hexdigest()[:32]
# Create payment
try:
payment = create_payment_request(
amount=amount_vnd,
description="Café 10-Punch Loyalty Card",
idempotency_key=idempotency_key
)
except Exception as e:
await update.message.reply_text(f"Error: {str(e)}")
return
# Send QR to customer
await update.message.reply_text(
f"Scan to pay:\n{payment.checkout_url}\n\nWaiting for payment..."
)
# Await settlement (non-blocking in production, use async/await)
try:
settlement = await_settlement(payment.id, timeout_seconds=300)
if settlement.status == "confirmed":
# Issue loyalty card
card_id = str(uuid.uuid4())
db.insert_card(user_id, card_id, punches=10)
await update.message.reply_text(
f"✅ Payment confirmed! Your card ID: {card_id}\n"
f"Punches: 10/10\n"
f"Show this to the café staff to start earning rewards."
)
else:
await update.message.reply_text(
f"Payment not confirmed. Status: {settlement.status}"
)
except TimeoutError:
await update.message.reply_text(
"Payment timed out. Please try again or contact support."
)
In production, you'd use webhooks instead of blocking await_settlement(). When the webhook fires, send the customer a message via Telegram API.
MCP Server Integration: Claude as Your Payment Agent
AgentPay VN includes an MCP (Model Context Protocol) server, letting Claude (or any Claude-compatible AI) invoke payment functions directly.
Configure Claude to Use AgentPay MCP
Add this to your Claude configuration JSON:
{
"mcpServers": {
"agentpay": {
"command": "python",
"args": ["-m", "agentpay_mcp.server"],
"env": {
"AGENTPAY_API_KEY": "your_api_key_here",
"WEBHOOK_SECRET": "your_webhook_secret"
}
}
}
}
Now Claude can invoke these functions via the MCP protocol:
agentpay_create_payment_request(amount, description, idempotency_key)agentpay_await_settlement(payment_id, timeout_seconds)agentpay_get_payment_status(payment_id)
Example Claude prompt:
"A customer wants to buy our 'Advanced Python' course for 599,000 VND. Create a payment request using AgentPay, send them the checkout URL, and wait for settlement. Once settled, confirm their purchase."
Claude will:
1. Call agentpay_create_payment_request(599000, "Advanced Python Course", ...)
2. Extract the checkout URL and present it to the user.
3. Call agentpay_await_settlement(...) to wait.
4. Respond when settlement is confirmed.
Do's and Don'ts: Common Pitfalls
| Do | Don't |
|---|---|
| Use deterministic idempotency keys (hash of order ID, customer ID, amount). | Generate random UUIDs as idempotency keys; this defeats idempotency. |
| Verify webhook signatures before processing. | Trust webhook payloads without verifying the signature. |
| Process webhooks idempotently (check if settlement already recorded). | Re-process the same webhook twice, causing duplicate records. |
| Return 200 OK within 5 seconds of receiving a webhook. | Perform long-running tasks in the webhook handler (e.g., send 1000 emails). |
| Retry payment creation on network timeouts and 5xx errors. | Retry on 4xx errors (these are validation failures, not transient issues). |
| Store the payment ID and idempotency key together in your database. | Store only one; you'll lose the ability to retry safely. |
Test webhooks locally using a tool like ngrok to expose your dev server. |
Test only in production; debug in the wild. |
Frequently Asked Questions
Q: What happens if my webhook endpoint is down when AgentPay tries to notify me?
A: AgentPay retries the webhook 5 times over 24 hours (immediately, 5 min, 30 min, 2 hr, 24 hr). Your endpoint must respond within 5 seconds. If all 5 attempts fail, check the Webhooks tab in the AgentPay dashboard to view failed events and manually process them.
Q: Can I poll for settlement status instead of using webhooks?
A: Yes. Use get_payment_status(payment_id) to poll the status. However, webhooks are faster and more reliable. In production, combine both: use webhooks for real-time updates and polling as a fallback (e.g., every 30 seconds for 10 minutes).
Q: How long does settlement typically take?
A: Most payments settle within 30 seconds to 5 minutes after the customer completes payment. It depends on the customer's bank. Check the settled_at timestamp in the webhook payload for exact timing.
Q: What if a customer pays but never receives their course access?
A: Check: 1. Did the webhook reach your endpoint? (Check logs.) 2. Did you record the settlement? (Query your database.) 3. Did you send the confirmation email? (Check your email service logs.)
If the webhook was never received, manually trigger the fulfillment action (send course access) from the AgentPay dashboard. AgentPay shows all settlements; match by payment ID.
Advanced: Reconciliation and Monitoring
In production, run a daily reconciliation job:
- Query AgentPay: Fetch all settlements from the last 24 hours.
- Query your database: Fetch all recorded settlements.
- Compare: If AgentPay has a settlement you don't, a webhook was lost; process it manually.
- Alert: If discrepancies exceed 0.01% of volume, escalate to engineering.
AgentPay's API provides a settlement list endpoint for this purpose. See the docs for details.
Key Takeaways
- Idempotency is non-negotiable: Use deterministic keys (hash of order ID, customer, amount) so retries never create duplicates.
- Webhooks are your settlement source of truth: Bank-feed webhooks confirm payment landed in the merchant account. Process them idempotently.
- The 3-line flow is simple but powerful:
create_payment_request()→ send checkout URL →await_settlement(). - Verify webhook signatures: Always confirm requests came from AgentPay using the shared secret.
- Fast, idempotent responses: Acknowledge webhooks within 5 seconds; defer long-running work to background jobs.
- Test locally with ngrok: Use ngrok to expose your dev server and test webhooks without deploying.
- Monitor and reconcile: Run daily reconciliation to catch missed webhooks before they become customer support tickets.
Get Started Now
AgentPay VN is open-source (MIT license) and production-ready. Install in seconds:
pip install agentpay-vn
Then run the MCP server:
agentpay-mcp
Read the full documentation and explore the GitHub repository for examples, source code, and contribution guidelines.
Your AI agents deserve reliable payments. AgentPay VN makes it happen.