Agent-native payments: idempotency, webhooks & settlement
The Problem: Your AI Agent Lost a Payment Confirmation
It's 2:14 AM. Your AI customer-service bot just helped a user buy a $200 online course. The agent created a VietQR payment request, sent the checkout link, and—network hiccup—the confirmation timed out. The agent retried. Then again.
By morning, you find three duplicate payment requests in your system, the customer received two checkout links, and your settlement report is confused about which one actually went through. The customer's bank shows one debit. Your database shows three requests. Welcome to the distributed systems nightmare that breaks naive payment flows.
This is where AgentPay VN changes the game. It's built from the ground up for AI agents to handle payments safely—with idempotency built in, webhooks to confirm reality, and a bank-driven settlement model that never touches your funds. Let's build it right.
Why Agent-Native Payment Architecture Matters
Traditional payment APIs were designed for humans clicking buttons on web forms. They assume: - One request = one outcome - The requester remembers what they did - Timeouts mean "I don't know what happened"
AI agents operate differently. They're stateless, they retry on failure, they can make the same logical request twice without realizing it. A standard Stripe or PayPal integration will generate duplicate charges. AgentPay VN flips this:
Idempotency by design: Every payment request gets a unique, agent-controlled ID. If the agent sends the same ID twice—intentionally or by accident—you get one payment, not two.
Webhooks as ground truth: The agent doesn't trust its own memory. Instead, a bank-confirmed webhook proves settlement happened. The agent waits for that signal, not for a timeout or a guessed response time.
Bank-direct flow: Money goes straight from customer's bank → merchant's bank. AgentPay VN generates the QR, the payment system routes it, but no intermediary ever holds the cash. Settlement is confirmed by the actual bank feed.
The 3-Line Agent Flow (And What Goes Wrong)
Here's the minimal happy path:
# Step 1: Create payment request with idempotency key
request = client.create_payment_request(
idempotency_key="order-2024-12-15-user-4521", # Must be unique per logical payment
amount=200_000, # VND
description="Online Course: Python Mastery"
)
# Step 2: Send checkout URL to customer
checkout_url = request.checkout_url
send_to_customer(checkout_url)
# Step 3: Wait for bank-confirmed settlement
settlement = client.await_settlement(
payment_id=request.id,
timeout_seconds=300 # 5-minute wait for most users
)
if settlement.confirmed:
grant_course_access(customer_id)
Simple, right? But now—what if the network fails between step 1 and 2? The payment request was created, but the agent never found out its ID. If it retries step 1 without an idempotency key, it creates a second request. Different QR code. Customer confusion.
The fix: Always include an idempotency_key—a string your agent controls that uniquely identifies the logical payment. If the agent crashes and restarts, it regenerates the same key for the same order, and AgentPay VN returns the original request instead of creating a duplicate.
Idempotency: The Shield Against Retry Chaos
Idempotency is the most underrated feature in payment systems. Here's why it matters for agents:
from agentpay_vn import AgentPayClient
import uuid
client = AgentPayClient(api_key="your-key-here")
# Agent logic: process an order from a queue
order_id = "order-2024-12-15-9875"
user_id = "user-4521"
amount = 150_000 # VND
# Generate a deterministic idempotency key
# (same order ID always produces same key)
idempotency_key = f"{order_id}:checkout"
try:
payment = client.create_payment_request(
idempotency_key=idempotency_key,
amount=amount,
description=f"Payment for order {order_id}",
customer_id=user_id # Track who's paying
)
print(f"QR Code: {payment.checkout_url}")
# Store payment.id for settlement lookup later
store_payment_reference(order_id, payment.id)
except Exception as e:
# Network timeout? Job queue crash? Doesn't matter.
# Next retry with same idempotency_key will return the original request.
logger.error(f"Payment creation failed: {e}")
# Agent will retry with same key → same result
Key insight: The idempotency key is not random. It's derived from the order/user/transaction context. This way, if your agent process restarts, it reconstructs the key and fetches the original payment request instead of duplicating it.
Do:
- Use deterministic keys: f"{order_id}:checkout" or f"{user_id}:{timestamp_hour}:payment"
- Store the returned payment.id immediately in your database
- Reuse the key if you need to retry within the same transaction
Don't: - Use random UUIDs as idempotency keys (defeats the purpose) - Change the key mid-retry (each key is a separate logical transaction) - Forget to store the payment ID (you need it for settlement confirmation)
Webhooks: How the Agent Knows Payment Actually Landed
This is the piece most naive implementations skip. Your agent creates a payment, sends a QR code, and then... waits? Polls? Guesses?
No. The agent subscribes to webhooks. When the customer's bank confirms funds moved to your account, AgentPay VN fires a webhook. That's your ground truth.
Setting up webhooks:
- Configure your webhook endpoint in AgentPay VN dashboard or via API
- Receive a settlement webhook when payment is confirmed by the bank
- Verify the signature (AgentPay VN signs every webhook)
- Update your agent's state based on the webhook
Here's a Flask webhook handler:
from flask import Flask, request
import hmac
import hashlib
app = Flask(__name__)
WEBHOOK_SECRET = "your-webhook-secret"
@app.route("/webhooks/settlement", methods=["POST"])
def handle_settlement_webhook():
"""
Receives settlement confirmation from AgentPay VN.
This is fired by the bank, not by the agent.
"""
payload = request.get_json()
signature = request.headers.get("X-AgentPay-Signature")
# Verify the webhook came from AgentPay VN
expected_sig = hmac.new(
WEBHOOK_SECRET.encode(),
request.get_data(),
hashlib.sha256
).hexdigest()
if not hmac.compare_digest(signature, expected_sig):
return {"error": "Invalid signature"}, 403
# Payload structure:
# {
# "event": "settlement.confirmed",
# "payment_id": "pay_abc123",
# "amount": 150000,
# "customer_id": "user_4521",
# "timestamp": "2024-12-15T14:30:00Z",
# "bank_reference": "VCB-2024-12-15-001234"
# }
if payload["event"] == "settlement.confirmed":
payment_id = payload["payment_id"]
customer_id = payload["customer_id"]
amount = payload["amount"]
# Find the order this payment belongs to
order = find_order_by_payment_id(payment_id)
# Grant access, send confirmation email, etc.
grant_course_access(customer_id, order.course_id)
send_receipt_email(customer_id, amount)
log_settlement(order.id, payment_id, "confirmed")
return {"status": "processed"}, 200
return {"status": "ignored"}, 200
if __name__ == "__main__":
app.run(port=5000)
Why this matters for agents: The agent doesn't need to poll or guess. It creates a payment, logs the payment ID, and moves on. When the webhook arrives, a background process (or your agent framework's event handler) fulfills the order. This decouples payment from action—the agent isn't blocked waiting for a user to scan the QR code.
MCP Server Integration: Claude Agents Pay Directly
If you're running AI agents via Claude (using Claude's Model Context Protocol), AgentPay VN provides an MCP server. This lets Claude directly invoke payment functions without writing HTTP code.
Install the MCP server:
pip install agentpay-mcp
Configure Claude to use it (in your Claude project settings):
{
"mcpServers": {
"agentpay": {
"command": "python",
"args": ["-m", "agentpay_mcp.server"],
"env": {
"AGENTPAY_API_KEY": "your-api-key-here",
"AGENTPAY_WEBHOOK_SECRET": "your-webhook-secret"
}
}
}
}
Now Claude can call payment functions like tools:
User: "I want to buy the Python course for 150,000 VND."
Claude: "I'll create a payment request for you. Let me use the AgentPay tool to generate a QR code."
[Claude invokes: agentpay.create_payment_request with idempotency_key, amount, description]
Claude: "Here's your checkout link: [QR/URL]. Please scan and confirm payment. I'll wait for settlement."
[Claude invokes: agentpay.await_settlement to monitor for webhook]
Claude: "Payment confirmed! Granting you course access now."
The MCP server handles all the auth, retries, and idempotency. Claude just calls tools.
Real-World Walkthrough: Online Course Bot
Let's build a complete flow: a customer service agent selling online courses.
Scenario: Customer: "I want to buy 'Advanced Python' course." Bot: "That's 200,000 VND. Let me create a payment link for you."
What happens under the hood:
- Agent generates order ID →
order-2024-12-15-8734 - Agent creates payment → idempotency_key =
"order-2024-12-15-8734:payment" - AgentPay VN returns → payment_id, checkout_url, QR code
- Agent sends QR to customer (email, SMS, chat)
- Customer scans QR → pays via their bank app
- Bank confirms → settlement webhook fires
- Webhook handler grants access → course unlocked in system
- Agent notifies customer → "Access granted! Start learning."
Code snapshot:
# In your agent framework (e.g., LangChain, AutoGen, Claude MCP)
async def sell_course(agent, customer_id, course_slug):
# 1. Lookup course price
course = db.get_course(course_slug) # 200,000 VND
# 2. Generate deterministic order ID
order_id = f"order-{date.today().isoformat()}-{uuid4().hex[:8]}"
# 3. Create payment with idempotency
payment = client.create_payment_request(
idempotency_key=f"{order_id}:payment",
amount=course.price_vnd,
description=f"Course: {course.title}",
customer_id=customer_id
)
# 4. Store for webhook lookup
db.insert_order({
"order_id": order_id,
"payment_id": payment.id,
"customer_id": customer_id,
"course_id": course.id,
"status": "awaiting_payment"
})
# 5. Send QR to customer
agent.send_message(f"Scan this QR to pay: {payment.checkout_url}")
# 6. Agent returns; webhook will fire when settled
# (In a real system, you'd also have a timeout fallback)
return {"order_id": order_id, "payment_id": payment.id}
Then, when webhook arrives:
# In your webhook handler
order = db.get_order_by_payment_id(payload["payment_id"])
course = db.get_course(order.course_id)
# Grant access
db.insert_enrollment({
"customer_id": order.customer_id,
"course_id": course.id,
"granted_at": now()
})
# Notify customer (via agent)
agent.send_notification(
customer_id=order.customer_id,
message=f"🎉 Access granted to '{course.title}'! Start learning: {course.start_url}"
)
Do's and Don'ts: Settlement Best Practices
| Do | Don't |
|---|---|
| Use deterministic idempotency keys | Generate random UUIDs for idempotency keys |
| Store payment.id immediately after creation | Assume payment.id is the same as order_id |
| Verify webhook signatures with HMAC | Trust webhook content without signature check |
| Implement webhook retry logic (exponential backoff) | Assume webhook fires exactly once |
| Set a reasonable await_settlement timeout (300-600s) | Wait indefinitely for settlement |
| Log every payment state transition | Rely on real-time polling |
| Test with staging API keys first | Push to production without testing |
| Handle webhook failures gracefully (queue for retry) | Crash if webhook processing fails |
Advanced Tips: Handling Edge Cases
Timeout Management
If the customer takes longer than 5 minutes to pay, your await_settlement() call will timeout. But the payment is still valid. Implement a background job that polls for settlement:
# After await_settlement times out
order = db.get_order(order_id)
if not order.is_settled:
# Schedule a background check in 1 minute
schedule_payment_check(order.payment_id, delay_seconds=60)
Duplicate Webhook Protection
Webhooks can fire multiple times (network retry, duplicate delivery). Use the webhook's timestamp + payment_id as a deduplication key:
dedup_key = f"{payload['payment_id']}:{payload['timestamp']}"
if db.webhook_seen(dedup_key):
return {"status": "deduplicated"}, 200 # Already processed
db.mark_webhook_processed(dedup_key)
# Process webhook
Refund Handling
AgentPay VN itself doesn't hold money, so refunds go directly to the customer's bank. If you need to refund, coordinate with your bank's API. Log the refund in your system and update the order status:
order.status = "refunded"
order.refund_reason = "Customer requested cancellation"
db.update_order(order)
FAQ: Common Agent Payment Questions
Q: What if the agent crashes between creating a payment and storing the payment_id?
A: On restart, the agent should call client.get_payment_request(idempotency_key=...) to fetch the original payment without creating a duplicate. Store the ID before doing anything else.
Q: How do I know if a webhook failed to deliver?
A: Implement a background job that checks unsettled orders every 5 minutes. If an order is settled according to the API but not marked as settled in your database, you know the webhook failed. Process the settlement manually.
Q: Can I use AgentPay VN for subscriptions?
A: Not directly—each payment is one-time. For subscriptions, create a new payment request each billing cycle with a unique idempotency key (e.g., "user-123:subscription:2024-12").
Q: Does AgentPay VN support partial payments or refunds?
A: Partial payments must be initiated by the agent as separate requests. Refunds are handled between you and your bank, not through AgentPay VN. Track refunds in your database.
Key Takeaways
- Idempotency is not optional: Use deterministic keys to prevent duplicate charges when agents retry.
- Webhooks are ground truth: Don't poll or guess. Wait for the bank to confirm via webhook.
- AgentPay VN never holds money: Funds go straight from customer bank → merchant bank. No intermediary.
- MCP server simplifies Claude integration: If using Claude, use the MCP server; Claude calls payment functions as tools.
- Settlement is event-driven: Structure your agent to log payment state and react to webhook events, not timeouts.
- Always verify webhook signatures: HMAC verification ensures webhooks actually came from AgentPay VN.
- Deterministic order IDs matter: They let you reconstruct the idempotency key after a crash.
Get Started Now
Your AI agent is ready to handle payments safely. Here's what to do next:
-
Install the SDK:
bash pip install agentpay-vn -
Read the full documentation: AgentPay VN Docs
-
Explore the source code: GitHub: agentpay-vn
-
Set up MCP for Claude:
bash pip install agentpay-mcp -
Test with your own agent: Start with the staging environment. Create a payment request, get a QR code, and simulate a settlement webhook. Verify idempotency by retrying with the same key.
Payment infrastructure for AI agents isn't just about collecting money—it's about building trust. AgentPay VN handles the complexity so your agent can focus on delivering value. Build boldly.