Agent-native payments: idempotency, webhooks, settlement

2026-10-10 · AgentPay VN

ai-agentspaymentspython-sdkwebhooksvietqr

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

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.

Get started →

← All posts