Agent-Native Payments: Idempotency, Webhooks & Settlement

2026-09-08 · AgentPay VN

paymentsai-agentsvietqrwebhookspython

The Problem: Why Agents Fail at Handling Money

Imagine you've built an AI assistant that books online courses. A student clicks "enroll," the agent creates a payment request, and—network hiccup—the connection drops. The agent retries. And retries again.

Now you have three payment requests for the same $50 course.

Or worse: the webhook confirming settlement never arrives, so your agent refunds the student and marks the course as free. Meanwhile, the money actually landed in your bank account yesterday.

This is the harsh reality of agent-native payments. Unlike traditional checkout forms where a human manually confirms their action, AI agents operate at machine speed, in loops, sometimes without human oversight. They need guarantees: that duplicate requests won't create duplicate charges, that settlement confirmation is ironclad, and that failures are recoverable.

AgentPay VN solves this by treating the payment flow as idempotent, webhook-driven, and bank-settled from day one. No escrow. No holding funds. Money goes straight to your merchant account, and you know exactly when it's there.

Why Idempotency Is Non-Negotiable for Agents

Idempotency means: making the same request 100 times produces the same result as making it once.

In agent workflows, this is critical because:

  1. Network retries are inevitable. Your agent might lose connection mid-request.
  2. Agents loop. They check "did payment succeed?" repeatedly until they get confirmation.
  3. No human review. Unlike a person who notices they clicked a button twice, an agent doesn't.

AgentPay VN bakes idempotency in via idempotency keys. When you create a payment request, you pass a unique identifier (usually based on the order ID or user + timestamp). If your agent crashes and retries with the same key, the system returns the existing payment request instead of creating a duplicate.

Request 1: idempotency_key="user_123_order_5678_1703001600"
  → Creates payment QR, returns checkout_url

Request 2 (retry): idempotency_key="user_123_order_5678_1703001600"
  → Returns *the same* checkout_url (no new QR)

Request 3 (another retry): idempotency_key="user_123_order_5678_1703001600"
  → Still the same QR

The customer, if they've already paid, sees the same QR. If they haven't, they scan it and pay. One transaction. One settlement.

How Webhooks Lock In Settlement Certainty

After a customer scans the VietQR code and transfers money, two things happen:

  1. The bank processes the transfer (usually within seconds to a few minutes).
  2. AgentPay VN receives a webhook from your merchant bank, confirming the funds arrived.

Your agent should never mark a payment as "settled" based on a timer or a single polling check. Webhooks are the source of truth. They're cryptographically signed and timestamped by the bank.

This means: - You know settlement happened (not "probably" or "in a few hours"). - Your agent can immediately trigger downstream actions: send course access, ship product, add credits. - You have a permanent audit log of every settlement event.

Step-by-Step: Building an Idempotent Agent Payment Flow

Step 1: Install AgentPay VN

pip install agentpay-vn

Step 2: Create a Payment Request with Idempotency

Here's a real agent workflow—say, a café selling coffee subscriptions:

from agentpay_vn import AgentPayClient
import hashlib
import time

# Initialize the client (reads AGENTPAY_API_KEY from env)
client = AgentPayClient()

def create_subscription_payment(customer_id: str, amount_vnd: int, plan_name: str):
    """
    Create a payment request for a coffee subscription.
    Uses an idempotency key to prevent duplicates if the agent retries.
    """
    # Step 1: Build a deterministic idempotency key
    # This key uniquely identifies this customer + plan + time window
    timestamp_bucket = int(time.time() / 60) * 60  # Round to nearest minute
    idempotency_key = hashlib.sha256(
        f"{customer_id}_{plan_name}_{timestamp_bucket}".encode()
    ).hexdigest()[:16]

    # Step 2: Create the payment request
    payment = client.create_payment_request(
        amount=amount_vnd,
        description=f"Coffee subscription: {plan_name}",
        idempotency_key=idempotency_key,
        metadata={
            "customer_id": customer_id,
            "plan": plan_name,
            "subscription": True
        }
    )

    # Step 3: Agent sends the checkout URL to the customer
    print(f"📱 Share this with customer:\n{payment.checkout_url}")
    print(f"🔑 Payment ID: {payment.id}")

    return payment

# Agent usage:
payment = create_subscription_payment(
    customer_id="cust_alice_001",
    amount_vnd=99000,  # ~$4 USD
    plan_name="premium_monthly"
)

Line-by-line explanation: - Lines 10–13: We create a deterministic idempotency key by hashing the customer ID, plan name, and a time bucket. If the agent retries within the same minute, it uses the same key. - Lines 15–24: We call create_payment_request() with the idempotency key. If this key has been used before, the SDK returns the existing payment instead of creating a new one. - Lines 27–30: We extract the checkout_url (the actual VietQR link) and send it to the customer. The agent now has a payment.id to track this transaction.

Step 3: Await Settlement via Webhook (Not Polling)

Don't do this:

# ❌ BAD: Polling in a loop
for i in range(60):
    status = client.get_payment_status(payment.id)
    if status == "settled":
        break
    time.sleep(5)

Do this instead:

def setup_settlement_webhook(agent_storage):
    """
    Register a webhook endpoint to handle bank settlements.
    This is the ground truth for payment confirmation.
    """
    def on_settlement(webhook_payload):
        payment_id = webhook_payload["payment_id"]
        amount = webhook_payload["amount"]
        timestamp = webhook_payload["settled_at"]

        # Webhook is cryptographically verified by AgentPay VN
        # Safe to trust this as the source of truth

        # Step 1: Log the settlement
        agent_storage.log_settlement(
            payment_id=payment_id,
            amount=amount,
            timestamp=timestamp
        )

        # Step 2: Trigger agent action (e.g., activate subscription)
        agent_storage.mark_subscription_active(payment_id)

        # Step 3: Send confirmation to customer
        customer_id = agent_storage.get_customer_by_payment(payment_id)
        send_email(customer_id, f"Your payment of {amount:,} VND confirmed!")

        return {"status": "ok"}

    return on_settlement

Why webhooks are superior: - No lag. You act the moment the bank confirms. - No missed events. If you were offline polling, you'd miss the settlement. - Cryptographic trust. AgentPay VN signs each webhook; your agent verifies the signature before acting.

Configuring AgentPay VN as an MCP Server for Claude

If you're using Claude or another LLM agent, integrate AgentPay VN as an MCP (Model Context Protocol) server:

{
  "name": "agentpay-mcp",
  "version": "1.0",
  "resources": [
    {
      "uri": "payment://create",
      "mimeType": "application/json",
      "description": "Create a new payment request",
      "parameters": {
        "amount": "integer (VND)",
        "description": "string",
        "idempotency_key": "string (unique identifier)",
        "metadata": "object (optional, for tracking)"
      }
    },
    {
      "uri": "payment://settle",
      "mimeType": "application/json",
      "description": "Confirm settlement via webhook",
      "parameters": {
        "payment_id": "string",
        "signature": "string (bank-signed)"
      }
    }
  ],
  "tools": [
    {
      "name": "create_payment",
      "description": "Create a VietQR payment request for an AI agent",
      "inputSchema": {
        "type": "object",
        "properties": {
          "amount": { "type": "number" },
          "description": { "type": "string" },
          "idempotency_key": { "type": "string" },
          "metadata": { "type": "object" }
        },
        "required": ["amount", "description", "idempotency_key"]
      }
    }
  ]
}

This allows Claude to call AgentPay VN directly via the MCP protocol, treating payment creation as a native capability.

Real-World Walkthrough: An Online Course Bot

Let's build an end-to-end scenario: an AI agent selling Python courses.

User: "I want to buy the Advanced Django course."

Agent flow:

  1. Create payment request (with idempotency key based on user + course): amount = 299_000 VND idempotency_key = hash("user_bob_django_advanced") → Payment ID: pay_1a2b3c4d → Checkout URL: vietqr.co/...

  2. Send checkout link to user.

  3. Customer scans QR with their banking app, transfers ₫299,000.

  4. Bank settlement webhook fires (within 2 minutes typically): json { "event": "payment.settled", "payment_id": "pay_1a2b3c4d", "amount": 299000, "settled_at": "2024-01-15T10:45:32Z", "signature": "...(bank-signed)..." }

  5. Agent receives webhook, verifies signature, and: - Marks payment as settled in the database. - Generates course access link. - Sends email: "Your course access: [link]. Enjoy!" - Logs the transaction for accounting.

  6. If agent crashes during step 3 and retries creating the payment: - Uses same idempotency key → gets same payment ID. - User sees same QR. - No duplicate charge.

Idempotency vs. Webhooks: Do's and Don'ts

✅ DO ❌ DON'T
Use deterministic idempotency keys based on user + item + time bucket Rely on random UUIDs; they won't deduplicate on retry
Listen for webhooks and act on settlement Poll get_payment_status() in a loop
Log webhook signatures for audit purposes Assume webhook came from the bank without verification
Retry payment requests with the same idempotency key Increment a counter and generate a new key on each retry
Store the payment ID and link it to your order ID Discard the payment ID and try to re-query by amount
Set a reasonable timeout for settlement (e.g., 10 minutes) Wait indefinitely; implement a fallback if webhook never arrives

Advanced Tips for Production Agents

Tip 1: Implement a Settlement Timeout

Webhooks are reliable, but network issues happen. Set a fallback:

import asyncio

async def wait_for_settlement(payment_id: str, timeout_seconds: int = 300):
    """
    Wait for settlement via webhook, but bail out after timeout.
    """
    settlement_event = asyncio.Event()

    # Register callback to fire when webhook arrives
    def on_webhook(event):
        if event["payment_id"] == payment_id:
            settlement_event.set()

    # Subscribe to webhook
    client.on_settlement(on_webhook)

    try:
        await asyncio.wait_for(
            settlement_event.wait(),
            timeout=timeout_seconds
        )
        return {"status": "settled", "via": "webhook"}
    except asyncio.TimeoutError:
        # Fallback: check payment status directly
        status = client.get_payment_status(payment_id)
        if status.settled:
            return {"status": "settled", "via": "fallback_check"}
        else:
            return {"status": "pending", "via": "timeout"}

Tip 2: Use Exponential Backoff for Retries

def create_payment_with_retry(amount, description, idempotency_key, max_attempts=3):
    """
    Retry payment creation with exponential backoff.
    The idempotency key ensures no duplicates even after multiple retries.
    """
    import time

    for attempt in range(max_attempts):
        try:
            return client.create_payment_request(
                amount=amount,
                description=description,
                idempotency_key=idempotency_key
            )
        except Exception as e:
            if attempt < max_attempts - 1:
                wait_time = 2 ** attempt  # 1s, 2s, 4s
                print(f"Attempt {attempt + 1} failed. Retrying in {wait_time}s...")
                time.sleep(wait_time)
            else:
                raise

Tip 3: Webhook Signature Verification

Always verify that webhooks come from AgentPay VN:

import hmac
import hashlib

def verify_webhook_signature(payload_bytes, signature_header, secret_key):
    """
    Verify that the webhook was signed by AgentPay VN.
    """
    expected_signature = hmac.new(
        secret_key.encode(),
        payload_bytes,
        hashlib.sha256
    ).hexdigest()

    return hmac.compare_digest(signature_header, expected_signature)

FAQ

Q: If I use the same idempotency key twice, will I be charged twice?

No. The SDK ensures that identical idempotency keys return the same payment request. The customer still pays only once, and the transaction settles once. Idempotency protects against duplicate charges caused by network retries or agent loops.

Q: What if the webhook never arrives?

Implement a timeout and fallback check (see Tip 1 above). After ~10 minutes, if the webhook hasn't arrived, your agent can call client.get_payment_status(payment_id) to check if the bank has confirmed settlement. The webhook is the preferred method because it's instant, but the fallback ensures you don't leave money on the table.

Q: Do you hold my money in escrow?

No. AgentPay VN is a payment flow coordinator, not a wallet. The VietQR points directly at your merchant bank account. When a customer transfers money, it lands in your account, not ours. A webhook from your bank confirms settlement. We never touch the funds.

Q: Can I use AgentPay VN outside Vietnam?

AgentPay VN uses VietQR, which is Vietnam's domestic QR code standard. It works for customers with Vietnamese bank accounts. International agents can initiate payments, but the payer must be in Vietnam with a compatible bank account.

Key Takeaways

Get Started Now

Ready to add idempotent, webhook-driven payments to your AI agent?

  1. Install AgentPay VN: bash pip install agentpay-vn

  2. Explore the full documentation: https://agentpay.servicesai.vn/v1/docs

  3. Check out the open-source code (MIT license): https://github.com/phuocdu/agentpay-vn

Your agents can now collect payments safely, reliably, and at scale. No more duplicate charges. No more mystery about settlement. Just pure, idempotent, webhook-driven Vietnamese payment flow.

Get started →

← All posts