Agent-Native Payments: Idempotency, Webhooks & Settlement

2026-08-20 · AgentPay VN

agentpayvietqrpaymentsai-agentswebhooks

The Problem: Why Your AI Agent Keeps Creating Duplicate Payments

Imagine you've built an AI chatbot that sells online courses. A customer asks to pay for a Python masterclass. Your agent calls a payment function, the network hiccups, and the agent retries. Now there are three identical payment requests in the system—each one charging the customer separately. By the time the bank settles, you've got a compliance nightmare, angry customers, and refund chaos.

This is the idempotency problem: in distributed systems, retries and network failures can trigger unintended duplicates. Traditional payment gateways solve this with idempotency keys. AgentPay VN brings that battle-tested pattern directly into the AI agent toolkit—letting you build payment flows that are safe by default, never holding customer funds, and settling instantly to your bank account via VietQR.

In this tutorial, you'll learn to architect agent-native payments that handle retries gracefully, confirm settlements reliably, and scale without fear.

How AgentPay VN Prevents Duplicate Charges

The Idempotency Key Pattern

AgentPay VN uses idempotency keys—unique identifiers you provide when creating a payment request. If your agent retries the same request with the same key, the system returns the existing payment instead of creating a duplicate.

Here's the core principle:

Idempotency Key = Hash(agent_id + customer_id + course_id + timestamp_bucket)

Why this matters for AI agents:

Real-World Scenario: E-Learning Bot

You have a Discord bot that sells Solidity courses. When a user types "buy course", the agent:

  1. Generates an idempotency key from user_id + course_id + current_hour.
  2. Calls create_payment_request() with that key.
  3. Sends the checkout URL.
  4. Waits for webhook confirmation.

If the webhook delivery fails and the bot retries, the idempotency key ensures only one payment record exists.

Setting Up Idempotent Payment Requests

Installation and Imports

First, install the SDK:

pip install agentpay-vn

Then initialize your agent:

from agentpay_vn import AgentPayClient
import hashlib
import json
from datetime import datetime, timedelta

# Initialize the client (uses AGENTPAY_API_KEY from environment)
client = AgentPayClient()

# Helper function to generate idempotency keys
def generate_idempotency_key(agent_id: str, customer_id: str, product_id: str) -> str:
    """
    Create a stable, scoped idempotency key.
    Using hourly bucket ensures the same customer buying the same product
    within one hour generates the same key (safe for retries).
    """
    now = datetime.utcnow()
    hour_bucket = now.replace(minute=0, second=0, microsecond=0).isoformat()

    key_material = f"{agent_id}:{customer_id}:{product_id}:{hour_bucket}"
    return hashlib.sha256(key_material.encode()).hexdigest()[:32]

Creating a Payment with Idempotency

async def sell_course(agent_id: str, customer_id: str, course_id: str):
    """
    Create an idempotent payment request for a course purchase.
    Multiple calls with the same customer/course/hour will return the same payment.
    """

    # Step 1: Generate stable idempotency key
    idempotency_key = generate_idempotency_key(
        agent_id=agent_id,
        customer_id=customer_id,
        product_id=course_id
    )

    # Step 2: Define payment details
    payment_data = {
        "amount": 299000,  # VND (Vietnamese Dong)
        "description": "Solidity Masterclass",
        "customer_email": "student@example.com",
        "customer_name": "Nguyen Van A",
        "metadata": {
            "course_id": course_id,
            "agent_id": agent_id,
            "utm_source": "discord_bot"
        }
    }

    # Step 3: Create payment request with idempotency key
    # If this call is retried, AgentPay VN returns the existing payment
    payment = await client.create_payment_request(
        amount=payment_data["amount"],
        description=payment_data["description"],
        customer_email=payment_data["customer_email"],
        customer_name=payment_data["customer_name"],
        metadata=payment_data["metadata"],
        idempotency_key=idempotency_key  # THE MAGIC LINE
    )

    # Step 4: Extract the checkout URL
    checkout_url = payment.checkout_url
    payment_id = payment.id

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

    return {
        "payment_id": payment_id,
        "checkout_url": checkout_url,
        "idempotency_key": idempotency_key
    }

Line-by-line explanation:

Webhooks and Settlement Confirmation

The Settlement Flow

AgentPay VN uses a bank feed to confirm when money actually lands in your account. Here's the flow:

  1. Customer pays → VietQR transfers funds to your bank immediately.
  2. Bank feed → AgentPay VN polls your bank (via API) and marks the payment as settled.
  3. Webhook → AgentPay sends a POST to your webhook URL with settlement confirmation.
  4. Your agent acts → Unlock the course, send an API key, email the receipt, etc.

Unlike traditional gateways that settle T+1 or T+3, VietQR settles in minutes because it's a direct bank-to-bank transfer.

Configuring Your Webhook Endpoint

Set your webhook URL in the AgentPay dashboard (or environment variable AGENTPAY_WEBHOOK_URL). Your endpoint should:

  1. Verify the webhook signature (to prevent replay attacks).
  2. Check the payment status.
  3. Idempotently handle the event (process only once, even if called twice).
from fastapi import FastAPI, Request
import hmac
import hashlib

app = FastAPI()

@app.post("/webhooks/agentpay")
async def handle_payment_settlement(request: Request):
    """
    Webhook handler for payment settlement confirmation.
    Called by AgentPay VN when a payment is marked as settled in the bank.
    """

    # Step 1: Verify webhook signature
    signature = request.headers.get("X-AgentPay-Signature")
    secret = "your_webhook_secret"  # Store securely in env vars
    body = await request.body()

    expected_sig = hmac.new(
        secret.encode(),
        body,
        hashlib.sha256
    ).hexdigest()

    if not hmac.compare_digest(signature, expected_sig):
        return {"status": "unauthorized"}, 401

    # Step 2: Parse the webhook payload
    payload = await request.json()
    payment_id = payload.get("payment_id")
    status = payload.get("status")  # e.g., "settled", "pending", "failed"
    amount = payload.get("amount")
    metadata = payload.get("metadata", {})

    # Step 3: Idempotently process the settlement
    # (Use payment_id as a unique key in your DB to prevent duplicate processing)
    if status == "settled":
        course_id = metadata.get("course_id")
        customer_email = payload.get("customer_email")

        # Check if we've already processed this payment
        existing = await db.settlements.find_one({"payment_id": payment_id})
        if existing:
            return {"status": "already_processed"}, 200

        # Now unlock the course and send credentials
        await unlock_course(customer_email, course_id)
        await send_access_email(customer_email, course_id)

        # Record that we processed it
        await db.settlements.insert_one({
            "payment_id": payment_id,
            "course_id": course_id,
            "amount": amount,
            "processed_at": datetime.utcnow()
        })

        return {"status": "processed"}, 200

    elif status == "failed":
        # Handle failed payments (refunds, retries, etc.)
        return {"status": "payment_failed"}, 200

    return {"status": "unknown"}, 400

Using AgentPay VN with Claude via MCP

The MCP (Model Context Protocol) server exposes AgentPay functions as tools for Claude:

{
  "name": "agentpay-mcp",
  "version": "1.0.0",
  "tools": [
    {
      "name": "create_payment_request",
      "description": "Create a VietQR payment request with idempotency key",
      "inputSchema": {
        "type": "object",
        "properties": {
          "amount": { "type": "integer", "description": "Amount in VND" },
          "description": { "type": "string" },
          "customer_email": { "type": "string" },
          "customer_name": { "type": "string" },
          "idempotency_key": { "type": "string", "description": "Unique key to prevent duplicates" },
          "metadata": { "type": "object" }
        },
        "required": ["amount", "description", "idempotency_key"]
      }
    },
    {
      "name": "get_payment_status",
      "description": "Poll payment status (settled, pending, failed)",
      "inputSchema": {
        "type": "object",
        "properties": {
          "payment_id": { "type": "string" }
        },
        "required": ["payment_id"]
      }
    },
    {
      "name": "await_settlement",
      "description": "Wait for payment to settle with timeout",
      "inputSchema": {
        "type": "object",
        "properties": {
          "payment_id": { "type": "string" },
          "timeout_seconds": { "type": "integer", "default": 300 }
        },
        "required": ["payment_id"]
      }
    }
  ]
}

To use it with Claude, install and configure the MCP server:

pip install agentpay-mcp

Then add to your Claude client configuration:

from anthropic import Anthropic

client = Anthropic(
    mcp_servers=[
        {"name": "agentpay", "command": "agentpay-mcp"}
    ]
)

response = client.messages.create(
    model="claude-3-5-sonnet-20241022",
    max_tokens=1024,
    tools=[...],  # MCP tools auto-loaded
    messages=[
        {
            "role": "user",
            "content": "I want to sell a course for 299,000 VND to john@example.com"
        }
    ]
)

Claude can now call create_payment_request directly, with idempotency keys generated automatically.

Real-World Walkthrough: Coffee Shop Loyalty Program

Let's build a complete example: a café chatbot that sells pre-paid loyalty cards.

The scenario: - Customers message a Facebook bot: "I want a 500k VND loyalty card." - Bot creates a payment request with idempotency key. - Customer scans QR, pays via their bank app. - Settlement webhook confirms receipt. - Bot sends a unique loyalty card code via SMS/email.

import asyncio
from agentpay_vn import AgentPayClient
from datetime import datetime
import hashlib

class CaféLoyaltyBot:
    def __init__(self):
        self.client = AgentPayClient()
        self.loyalty_cards = {}  # payment_id -> card_code

    def gen_card_code(self, payment_id: str) -> str:
        """Generate a unique loyalty card code."""
        return hashlib.md5(f"{payment_id}:cafe:loyaltyv1".encode()).hexdigest()[:12].upper()

    async def sell_loyalty_card(self, customer_phone: str, amount_vnd: int = 500000):
        """
        Main flow: Create payment, return checkout URL, wait for settlement.
        """
        # Generate idempotency key (stable for 1 hour)
        now = datetime.utcnow()
        hour_bucket = now.replace(minute=0, second=0, microsecond=0).isoformat()
        idem_key = hashlib.sha256(
            f"cafe:{customer_phone}:{amount_vnd}:{hour_bucket}".encode()
        ).hexdigest()[:32]

        # Create payment
        print(f"[Café Bot] Creating {amount_vnd} VND payment for {customer_phone}...")
        payment = await self.client.create_payment_request(
            amount=amount_vnd,
            description="Loyalty Card (500k VND balance)",
            customer_name=customer_phone,
            customer_email="loyalty@cafe.local",
            metadata={"phone": customer_phone, "type": "loyalty_card"},
            idempotency_key=idem_key
        )

        payment_id = payment.id
        checkout_url = payment.checkout_url

        print(f"[Café Bot] Checkout URL: {checkout_url}")
        print(f"[Café Bot] Waiting for settlement (max 5 minutes)...")

        # Wait for settlement (blocks until settled or timeout)
        try:
            settled_payment = await self.client.await_settlement(
                payment_id=payment_id,
                timeout_seconds=300
            )

            if settled_payment.status == "settled":
                # Payment confirmed! Generate and send loyalty card
                card_code = self.gen_card_code(payment_id)
                self.loyalty_cards[payment_id] = card_code

                print(f"[Café Bot] ✓ Payment settled!")
                print(f"[Café Bot] Loyalty card: {card_code}")
                print(f"[Café Bot] SMS sent to {customer_phone}: 'Your café loyalty card: {card_code}'")

                return {
                    "status": "success",
                    "payment_id": payment_id,
                    "card_code": card_code,
                    "message": f"Your loyalty card is ready: {card_code}"
                }

        except asyncio.TimeoutError:
            print(f"[Café Bot] ✗ Payment not settled within 5 minutes.")
            return {
                "status": "timeout",
                "payment_id": payment_id,
                "checkout_url": checkout_url,
                "message": "Please complete payment. Check status later."
            }

    async def check_status(self, payment_id: str):
        """Polling endpoint for customer to check their loyalty card."""
        status = await self.client.get_payment_status(payment_id)

        if status.status == "settled" and payment_id in self.loyalty_cards:
            return {"status": "ready", "card_code": self.loyalty_cards[payment_id]}
        elif status.status == "settled":
            # Generate it now (in case bot restarted)
            card_code = self.gen_card_code(payment_id)
            self.loyalty_cards[payment_id] = card_code
            return {"status": "ready", "card_code": card_code}
        else:
            return {"status": status.status, "message": "Still processing..."}

# Usage
bot = CaféLoyaltyBot()
await bot.sell_loyalty_card(customer_phone="+84912345678", amount_vnd=500000)

Why this works:

  1. Idempotency key scoped to phone + hour means a customer can't accidentally create two payments by requesting twice.
  2. await_settlement() blocks until the bank confirms the transfer, eliminating race conditions.
  3. No holding funds: Money goes straight to the café's bank account; AgentPay VN is just a notification layer.
  4. Webhook fallback: Even if await_settlement() times out, the webhook handler will eventually trigger and unlock the card.

Do's and Don'ts

Do Don't
✅ Use stable idempotency keys (based on user + product + time bucket) ❌ Generate random keys for every call—defeats the purpose
✅ Log payment_id and idempotency_key for debugging ❌ Assume retries are rare; plan for network failures
✅ Verify webhook signatures before processing ❌ Trust webhook content without validation
✅ Use await_settlement() with a timeout; have a fallback ❌ Hang forever waiting for settlement
✅ Store settlement confirmations in your DB to idempotently handle duplicate webhooks ❌ Process the same webhook twice by accident
✅ Test with real QR payments in staging first ❌ Assume prod behavior matches dev

FAQ

Q: What happens if my webhook endpoint goes down? A: AgentPay VN retries the webhook for 24 hours with exponential backoff. In the meantime, your agent can poll get_payment_status() to check if a payment settled. Idempotent webhook handlers ensure duplicate retries don't cause harm.

Q: How do I handle payments that fail? A: The webhook will include status: "failed". Your agent should notify the customer and optionally re-offer the payment (with a fresh idempotency key for a new hour bucket if they retry).

Q: Can I refund a settled payment? A: AgentPay VN itself doesn't hold funds, so refunds are processed through your bank's APIs. The payment_id and metadata in AgentPay help you reconcile; the actual refund happens outside AgentPay.

Q: Is there a minimum/maximum amount? A: VietQR supports amounts from 1,000 VND to 100,000,000 VND. AgentPay VN inherits these limits. Check the docs for current policies.

Key Takeaways

Getting Started

Install AgentPay VN now:

pip install agentpay-vn

Then install the MCP server for Claude integration:

pip install agentpay-mcp

Read the full documentation and explore examples at https://agentpay.servicesai.vn/v1/docs and browse the source code on GitHub.

Start building safer, faster payment flows for your AI agents today.

Get started →

← All posts