Agent-Native Payments: Idempotency, Webhooks & Settlement

2026-08-08 · AgentPay VN

ai-agentspaymentsvietqrpython-sdkwebhooks

The Unreliable Agent Problem

You've built an AI agent that sells digital courses. A customer asks your agent: "I want to enroll in the Python course." The agent creates a payment request and sends the checkout URL. But then—the customer's network hiccups. The agent doesn't receive confirmation. Does it create another payment request? Now the customer sees two identical QR codes. They pay twice. Your support team gets a panicked message at 2 AM.

This scenario repeats across thousands of AI agent integrations. Without idempotency and proper settlement tracking, payment systems become liability machines, not revenue engines. AgentPay VN solves this with a bank-aware architecture: the QR points directly at the merchant's bank account, webhooks confirm settlement in real time, and idempotency keys prevent accidental duplicate charges.

Let's build a payment system that AI agents can trust.

Understanding Idempotency in Agent Workflows

Why Agents Break Payment Systems

Traditional payment integrations assume a synchronous, single-request user flow. An agent works differently:

Idempotency is the fix. An idempotency key—a unique identifier you provide—ensures that identical requests produce identical results, no matter how many times they're sent.

AgentPay VN's Idempotency Model

AgentPay VN requires an idempotency_key in every create_payment_request call. This key is scoped to your merchant account, so you can safely retry without side effects:

from agentpay_vn import PaymentClient

client = PaymentClient(api_key="your_api_key")

# First request with idempotency key
response = client.create_payment_request(
    amount=500000,  # 500,000 VND
    description="Python Course - Intermediate Level",
    idempotency_key="order_user123_2025_01_15_attempt_1",  # Unique per transaction
    metadata={
        "student_id": "user_123",
        "course_id": "python_intermediate",
        "agent_id": "enrollment_bot_v2"
    }
)

checkout_url = response["checkout_url"]
print(f"Send this URL to customer: {checkout_url}")

# Network fails. Agent retries with the SAME idempotency key.
# AgentPay returns the same response—no duplicate charge.
retry_response = client.create_payment_request(
    amount=500000,
    description="Python Course - Intermediate Level",
    idempotency_key="order_user123_2025_01_15_attempt_1",  # Same key
    metadata={
        "student_id": "user_123",
        "course_id": "python_intermediate",
        "agent_id": "enrollment_bot_v2"
    }
)

# retry_response["checkout_url"] == checkout_url

Line-by-line breakdown:

  1. PaymentClient initializes with your API key (from https://agentpay.servicesai.vn/v1/docs).
  2. create_payment_request takes the amount in VND, a human description, and the critical idempotency_key.
  3. The key format I recommend: order_{user_id}_{date}_{attempt_counter}. This prevents collisions across your system.
  4. metadata stores agent context—helpful for reconciliation and debugging.
  5. On retry (identical idempotency key), the API returns the cached response instantly. No new charge.

Webhooks: Real-Time Settlement Confirmation

Why Polling Isn't Enough

You could call await_settlement() in a loop every 2 seconds. But that's wasteful: 43,200 API calls per day per active transaction. Webhooks flip the model: the bank notifies you when payment settles.

AgentPay VN's bank feed reads the merchant's actual bank account. When a VietQR payment lands, AgentPay fires a webhook to your endpoint. This is the source of truth—the payment actually cleared.

Setting Up Webhook Handlers

You need an HTTPS endpoint that receives POST requests:

from flask import Flask, request, jsonify
import hmac
import hashlib
import json
from agentpay_vn import verify_webhook_signature

app = Flask(__name__)
WEBHOOK_SECRET = "your_webhook_secret"  # Get this from the AgentPay dashboard

@app.route("/webhooks/agentpay", methods=["POST"])
def handle_settlement_webhook():
    """
    Receives webhook when a VietQR payment settles.
    The bank feed confirms this is real money in your account.
    """
    payload = request.get_data(as_text=True)
    signature = request.headers.get("X-Signature")

    # Verify the webhook came from AgentPay (not a spoofed request)
    if not verify_webhook_signature(payload, signature, WEBHOOK_SECRET):
        return jsonify({"error": "Invalid signature"}), 403

    data = json.loads(payload)

    # data now contains:
    # {
    #   "event": "payment.settled",
    #   "payment_id": "pay_xyz123",
    #   "idempotency_key": "order_user123_2025_01_15_attempt_1",
    #   "amount": 500000,
    #   "status": "settled",
    #   "timestamp": "2025-01-15T14:32:00Z",
    #   "bank_ref": "VietQR_abc123"
    # }

    if data["event"] == "payment.settled":
        payment_id = data["payment_id"]
        idempotency_key = data["idempotency_key"]
        amount = data["amount"]

        # Update your database atomically
        # Mark enrollment as "PAID"
        # Trigger course access provisioning
        activate_student_enrollment(idempotency_key, amount)

        # Send agent a signal to continue workflow
        notify_enrollment_agent(payment_id, status="complete")

        return jsonify({"status": "processed"}), 200

    return jsonify({"status": "ignored"}), 200

def activate_student_enrollment(idempotency_key, amount):
    """
    1. Extract student_id from the idempotency key or your idempotency_key→metadata mapping.
    n2. Mark payment as settled in your database.
    3. Grant course access immediately.
    """
    # Your application logic
    pass

if __name__ == "__main__":
    # Run on HTTPS (required for webhooks)
    app.run(ssl_context="adhoc", port=443, host="0.0.0.0")

Key points:

Settlement Confirmation: The Full Flow

Creating and Tracking a Payment

Here's a realistic 3-step flow an agent executes:

import asyncio
from agentpay_vn import PaymentClient
import uuid
from datetime import datetime

client = PaymentClient(api_key="your_api_key")

async def agent_sell_course(student_id: str, course_id: str, amount_vnd: int):
    """
    An AI agent handles the entire enrollment flow.
    This is safe to retry because of idempotency.
    """

    # Step 1: Generate a stable idempotency key
    timestamp = datetime.utcnow().isoformat()
    idempotency_key = f"order_{student_id}_{course_id}_{timestamp}"

    print(f"[Agent] Creating payment request for {student_id}...")

    # Step 2: Create payment request (idempotent)
    payment = client.create_payment_request(
        amount=amount_vnd,
        description=f"Course enrollment: {course_id}",
        idempotency_key=idempotency_key,
        metadata={
            "student_id": student_id,
            "course_id": course_id,
            "agent_version": "2.1.0"
        }
    )

    payment_id = payment["id"]
    checkout_url = payment["checkout_url"]

    print(f"[Agent] Payment created: {payment_id}")
    print(f"[Agent] Checkout URL: {checkout_url}")

    # Step 3: Wait for settlement (via webhook or polling)
    print(f"[Agent] Awaiting settlement...")

    settlement = await client.await_settlement(
        payment_id=payment_id,
        timeout_seconds=3600  # 1 hour timeout
    )

    if settlement["status"] == "settled":
        print(f"[Agent] ✓ Payment settled! Amount: {settlement['amount']} VND")
        print(f"[Agent] Bank reference: {settlement['bank_ref']}")

        # Trigger course access provisioning
        provision_course_access(student_id, course_id)
        return {"success": True, "payment_id": payment_id}
    else:
        print(f"[Agent] ✗ Payment failed: {settlement['status']}")
        return {"success": False, "payment_id": payment_id}

def provision_course_access(student_id: str, course_id: str):
    """Unlock the course for the student."""
    print(f"[System] Provisioning {course_id} access for {student_id}")
    # Your enrollment database logic here

# Agent executes the workflow
result = asyncio.run(agent_sell_course(
    student_id="student_5678",
    course_id="python_intermediate",
    amount_vnd=500000
))

Why this is resilient:

MCP Server for Claude & Other AI Agents

If you're using Claude or another AI agent framework, AgentPay VN provides an MCP (Model Context Protocol) server:

pip install agentpay-vn
agentpay-mcp --api-key your_api_key --webhook-secret your_webhook_secret

Configure Claude with this MCP server in your Claude app's config:

{
  "mcpServers": {
    "agentpay": {
      "command": "agentpay-mcp",
      "args": [
        "--api-key",
        "${AGENTPAY_API_KEY}",
        "--webhook-secret",
        "${AGENTPAY_WEBHOOK_SECRET}"
      ]
    }
  }
}

Now Claude can call:

This lets Claude autonomously handle payments in multi-turn conversations:

Claude prompt example:

You are a course enrollment assistant. When a user wants to buy a course:
1. Call agentpay_create_payment_request with amount, description, and a unique idempotency_key.
2. Give the user the checkout_url.
3. Poll agentpay_check_settlement every 5 seconds until settled.
4. Once settled, confirm enrollment and provide access instructions.

Real-World Walkthrough: Online Café Subscription

The Scenario

You run an online specialty coffee shop. Customers can purchase 10-coffee subscription boxes. Your bot handles the entire sales conversation.

The Flow

  1. Customer messages bot: "I want a subscription box."
  2. Bot creates payment request: python idempotency_key = f"coffee_sub_{customer_id}_{uuid.uuid4()}" payment = client.create_payment_request( amount=450000, # 450k VND for 10 specialty coffees description="Premium Coffee Subscription Box (10 packs)", idempotency_key=idempotency_key, metadata={"customer_id": customer_id, "product": "sub_box_v1"} )
  3. Bot sends checkout URL in the chat.
  4. Customer scans VietQR with their bank app. Confirms payment.
  5. Bank processes payment, routes it to your merchant account.
  6. Webhook firespayment.settled event.
  7. Your system: - Marks subscription as "ACTIVE" - Triggers warehouse to pick and ship coffee box - Sends bot a settlement_confirmed signal
  8. Bot replies: "✓ Your subscription is confirmed! Shipping address on file: [address]. You'll receive your coffee within 3 days."

Safety guarantees:

Do's and Don'ts: Best Practices

✅ Do ❌ Don't
Use deterministic idempotency_key (include user ID, timestamp, order ID) Use random UUIDs for idempotency keys—they defeat the purpose
Verify webhook signatures with verify_webhook_signature Trust unsigned webhooks or skip verification
Store idempotency key → payment ID mapping in your database Rely on agent memory alone; agents can't reliably retain state
Wait for settlement webhook before provisioning access Assume payment succeeded just because checkout_url was generated
Set reasonable timeout_seconds on await_settlement (300–3600s) Wait indefinitely or timeout instantly
Log bank_ref from settlement for reconciliation Discard bank references—you'll need them for audit trails
Use HTTPS for webhook endpoints Expose webhooks over HTTP—payment data in transit must be encrypted
Test with small amounts (1,000–10,000 VND) first Deploy payment logic to production without staging tests

FAQ: Common Agent Payment Questions

Q: Can an agent call create_payment_request twice by mistake?

A: Yes, but it's harmless. If it uses the same idempotency_key, AgentPay returns the cached response—no new charge. If it uses a different key, a new payment request is created (your checkout URL list grows, but the customer only pays once). Always use the same idempotency key for a logical transaction.

Q: What if the webhook never arrives?

A: await_settlement() has a built-in timeout. After timeout_seconds, it returns the latest known status (polled from the bank). Webhooks are optimizations, not requirements. The bank feed is the source of truth.

Q: How long does settlement take?

A: Typically 5–30 seconds from payment scan to webhook fire. The bank feed queries your merchant account every 30 seconds. So worst-case latency is ~60 seconds. If you need real-time (< 1 second), discuss with AgentPay—some bank integrations offer faster feeds.

Q: Can I refund a settled payment?

A: Not via AgentPay VN (it doesn't hold funds). Money goes straight to your bank. Refunds happen in your bank's portal or via your bank's API. AgentPay tracks what settled; you handle reversals. Always validate bank_ref for refund records.

Key Takeaways

Getting Started

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

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

  3. Explore the GitHub repo for examples: https://github.com/phuocdu/agentpay-vn

  4. Deploy the MCP server if you're using Claude or another agent framework.

  5. Start with a test payment (1,000–10,000 VND). Verify idempotency by retrying with the same key. Confirm webhook delivery. Then go live.

Your AI agents can now collect payments safely, idempotently, and with full settlement visibility. Build with confidence.

Get started →

← All posts