Agent-native payments: idempotency, webhooks, settlement
The silent killer of agent payments: the double-charge scenario
Imagine your AI chatbot is selling online courses. A customer asks to enroll in a $49 course. Your agent creates a payment request, sends the checkout URL, and waits. The network hiccups. Your agent times out, retries the request—and now the customer sees two identical payment QR codes. Panic. Refund requests. Lost trust.
This is the real-world nightmare that makes payment integration terrifying for agent developers. Unlike a traditional web form where a user clicks once, AI agents operate in loops, retry logic, and concurrent workflows. A single network stutter can trigger duplicate transactions, settlement chaos, and reconciliation headaches.
AgentPay VN solves this by baking idempotency, webhook reliability, and settlement tracking into its core design—three pillars that transform payments from a liability into a transparent, auditable asset for your agent.
Understanding idempotency: the guardrail you didn't know you needed
Idempotency means performing the same operation multiple times produces the same result as performing it once. In payments, this is non-negotiable.
AgentPay VN uses an idempotency key—a unique identifier you provide when creating a payment request. If your agent crashes, retries, or the network flakes, the same idempotency key guarantees the SDK returns the same payment request, not a duplicate charge.
Why this matters: - Your agent can safely retry failed requests without creating ghost transactions - Webhook delivery fails? Retry without fear—the settlement is already recorded - Multi-step agent workflows can pause, resume, or fork without payment confusion
Let's see this in action:
from agentpay_vn import AgentPay
import uuid
# Initialize the SDK with your merchant config
agent_pay = AgentPay(
merchant_id="YOUR_MERCHANT_ID",
api_key="YOUR_API_KEY"
)
# Generate a unique idempotency key (e.g., order ID + timestamp)
order_id = "order_20250115_user123"
idempotency_key = f"{order_id}_{uuid.uuid4().hex[:8]}"
# First call: creates payment request
payment_1 = agent_pay.create_payment_request(
amount=4900, # $49.00 in cents
description="Online Course: Python for AI Agents",
customer_name="Alice",
customer_phone="0912345678",
idempotency_key=idempotency_key
)
print(f"Payment ID: {payment_1['id']}")
print(f"Checkout URL: {payment_1['checkout_url']}")
# Network timeout... agent retries with the SAME idempotency_key
print("\n--- Agent retries after timeout ---")
payment_2 = agent_pay.create_payment_request(
amount=4900,
description="Online Course: Python for AI Agents",
customer_name="Alice",
customer_phone="0912345678",
idempotency_key=idempotency_key # Same key
)
# Both calls return identical payment_id and checkout_url
assert payment_1['id'] == payment_2['id'], "Idempotency key works!"
print("✓ Same payment request returned—no duplicate charge")
Key insight: The idempotency key is your agent's seatbelt. It lets you write retry logic with confidence, knowing that transient failures won't cascade into financial disasters.
Webhooks: the communication backbone your agent needs
AgentPay VN's settlement flow is event-driven. When a customer pays or when the bank confirms settlement, a webhook fires. Your agent catches it, updates its database, and continues its workflow.
This is fundamentally different from polling (repeatedly asking "Is the money here yet?"). Webhooks are: - Instant: Settlement confirmed within seconds of bank confirmation - Reliable: AgentPay retries failed webhook deliveries with exponential backoff - Auditable: Every event is logged for compliance and debugging
Webhook flow: 1. Customer scans QR → pays via VietQR 2. Bank confirms → AgentPay fires webhook to your endpoint 3. Your agent receives webhook, verifies signature, updates order status 4. Agent proceeds: send course access, trigger fulfillment, log settlement
Setup is simple. Configure a public HTTPS webhook endpoint and register it in AgentPay's dashboard. On the MCP server (Model Context Protocol), Claude or your favorite AI agent can call register_webhook to bind the endpoint.
{
"name": "agentpay-mcp",
"type": "stdio",
"command": "agentpay-mcp",
"env": {
"AGENTPAY_MERCHANT_ID": "your_merchant_id",
"AGENTPAY_API_KEY": "your_api_key",
"WEBHOOK_SECRET": "your_webhook_signing_secret"
}
}
Add this to your Claude cline_mcp_config.json or claude_desktop_config.json, then Claude can directly invoke AgentPay operations:
Tools available:
- create_payment_request(amount, description, customer_name, idempotency_key)
- await_settlement(payment_id, timeout_seconds)
- verify_webhook(body, signature)
- get_payment_status(payment_id)
Await settlement: blocking on certainty, not hope
The third pillar is await_settlement. Rather than polling or guessing, your agent explicitly waits for the bank feed to confirm money landed in your account.
import asyncio
from agentpay_vn import AgentPay
agent_pay = AgentPay(
merchant_id="YOUR_MERCHANT_ID",
api_key="YOUR_API_KEY"
)
async def enroll_student(student_email: str, course_id: str):
"""
Enroll a student only after payment settles.
Uses idempotency to handle retries safely.
"""
# Step 1: Create payment request
idempotency_key = f"course_enroll_{student_email}_{course_id}"
payment = agent_pay.create_payment_request(
amount=4900, # $49.00
description=f"Course enrollment: {course_id}",
customer_name=student_email,
customer_phone="", # Optional
idempotency_key=idempotency_key
)
payment_id = payment['id']
checkout_url = payment['checkout_url']
print(f"Student {student_email} → {checkout_url}")
# Agent sends this URL to student (via SMS, email, or chat)
# Step 2: Block until settlement confirmed
# timeout_seconds=600 means wait up to 10 minutes
settlement = await agent_pay.await_settlement(
payment_id=payment_id,
timeout_seconds=600
)
if settlement['status'] == 'settled':
print(f"✓ Payment settled: {settlement['amount_cents']} cents")
print(f" Bank reference: {settlement['bank_reference']}")
# Step 3: Grant course access (only after settlement confirmed)
# This is safe—idempotency key ensures no duplicate enrollments
send_course_access_email(student_email, course_id)
log_enrollment(
student=student_email,
course=course_id,
payment_id=payment_id,
settlement_ref=settlement['bank_reference']
)
return {"status": "enrolled", "message": f"Welcome to {course_id}!"}
elif settlement['status'] == 'timeout':
print(f"✗ Payment not received within 10 minutes")
return {"status": "pending", "message": "Please complete payment soon"}
else:
print(f"✗ Payment failed: {settlement['reason']}")
return {"status": "failed", "message": "Payment declined. Please try again."}
# Run the agent workflow
asyncio.run(enroll_student("alice@example.com", "python-agents-101"))
Why await_settlement is revolutionary:
- No guessing: bank feed confirms actual money in your account
- Async-friendly: your agent continues other tasks while waiting
- Timeout-safe: won't block forever if payment fails
- Audit trail: every settlement is logged with bank reference
Real-world walkthrough: AI café ordering bot
Imagine a café bot that takes orders via WhatsApp and accepts payment via VietQR.
Customer flow:
1. User: "I want 2 cappuccinos and a croissant"
2. Bot calculates total: 120,000 VND (~$5)
3. Bot creates payment request with idempotency key cafe_order_user456_2025011501
4. Bot sends: "Scan this QR to pay → [checkout_url]"
5. Customer scans and pays
6. Bank confirms → webhook fires
7. Bot receives settlement notification
8. Bot sends: "✓ Order confirmed! Ready in 15 min. Pickup at counter 3."
9. Order fulfillment system notified automatically
Key advantages: - Zero float: Money hits the café's business account immediately after customer payment (no intermediate holding) - Retry-safe: If bot crashes mid-order, restarting with same idempotency key doesn't duplicate charges - Verifiable: Bank reference in settlement log proves both bot and café have same ground truth
Idempotency, webhooks, and settlement: do's and don'ts
| Do | Don't |
|---|---|
| Generate idempotency key from stable data (order ID + user ID) | Use random UUIDs as idempotency key—you lose replay safety |
| Verify webhook signatures before trusting event data | Trust webhook payload without HMAC validation |
| Await settlement before granting access or fulfilling | Fulfill immediately after seeing checkout_url |
| Log idempotency key and payment ID together in database | Skip audit logs—compliance will hurt |
| Retry failed webhook deliveries with exponential backoff | Retry immediately in tight loop—risk rate-limiting |
| Handle settlement timeout gracefully (notify user) | Let awaits block indefinitely |
| Use MCP server for Claude to orchestrate payments | Embed credentials in agent prompts |
Advanced tips: hardening your agent payment flow
1. Idempotency key formula: Build deterministic idempotency keys so retries always match:
import hashlib
def make_idempotency_key(order_id: str, customer_id: str, amount_cents: int) -> str:
data = f"{order_id}|{customer_id}|{amount_cents}"
return hashlib.sha256(data.encode()).hexdigest()[:32]
# This is deterministic: same inputs → same key
# Agent can recompute it after crash without consulting database
2. Webhook signature verification: Always validate the HMAC signature AgentPay includes:
import hmac
import hashlib
def verify_webhook(body: str, signature: str, secret: str) -> bool:
expected = hmac.new(
secret.encode(),
body.encode(),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected)
3. Settlement state machine:
Model settlement as a state machine in your database:
- pending: Payment request created, awaiting customer action
- processing: Customer paid, bank confirming (usually <30s)
- settled: Bank confirmed, money in merchant account
- failed: Payment declined or timed out
- refunded: Manual reversal initiated
This clarity prevents agent logic bugs where you forget a state.
FAQ
Q: What if my agent crashes after creating a payment but before awaiting settlement?
A: Restart your agent with the same idempotency key. Call create_payment_request again—you'll get the same payment ID and checkout URL. The customer's payment (if they made one) is unaffected. Resume await_settlement with that payment ID.
Q: How long do webhooks retry if my server is down?
A: AgentPay retries with exponential backoff for 24 hours. During that window, your agent can come back online and catch up. Check get_payment_status to fetch current state.
Q: Does AgentPay hold my money? A: No. The VietQR points directly at your merchant bank account. Settlement is confirmed by your bank's feed, not AgentPay. AgentPay is purely the orchestration layer.
Q: Can I use await_settlement in Claude's tool calls?
A: Yes. Register AgentPay MCP server in Claude's config, and Claude can call await_settlement just like any other tool. It's async-safe.
Key takeaways
- Idempotency keys are your safety net: They eliminate duplicate charges when agents retry. Use deterministic keys tied to order data.
- Webhooks are real-time, not polling: Bank settlement fires an event instantly. Your agent can react within seconds, not minutes.
- Await settlement before fulfilling: Don't send course access, unlock content, or fulfill orders until the bank feed confirms payment.
await_settlementis your guard. - AgentPay never touches your money: It's orchestration-only. Money flows directly to your merchant account via VietQR.
- MCP integration lets Claude be your payment agent: Embed the MCP server in your Claude config and let it handle payment flows natively.
- Audit everything: Log idempotency keys, payment IDs, settlement references, and webhook events. This is your compliance record and debugging lifeline.
Get started now
Install AgentPay VN and start building payment-native agents:
pip install agentpay-vn
Start with the docs at https://agentpay.servicesai.vn/v1/docs.
For MCP server integration, install:
pip install agentpay-mcp
Explore the full source on GitHub: https://github.com/phuocdu/agentpay-vn.
Build agents that move money—reliably, transparently, and without the headaches.