Agent-Native Payments: Idempotency, Webhooks & Settlement

2026-09-06 · AgentPay VN

paymentsai-agentsvietqrwebhookspython

The Problem: Why Your AI Agent's Payment Flow Keeps Failing

Imagine your course-selling chatbot just processed a student's enrollment. The agent calls your payment API, gets a temporary network hiccup, retries, and suddenly the student's card is charged twice—but the agent doesn't know it. Or worse: the webhook confirming settlement never arrives, so your bot sits there indefinitely, unable to unlock the course material.

This is the harsh reality of AI agents handling money. Unlike traditional web apps where a human refreshes a page or sees an error, agents operate autonomously. They need idempotency (doing the same thing twice produces the same result), reliable webhooks (knowing when payment actually settled), and settlement confirmation (proof the merchant's bank account received funds).

AgentPay VN solves this with a three-line flow backed by VietQR infrastructure. But to use it correctly—especially in production—you need to understand the why behind each pattern.

Why Idempotency Matters for Autonomous Systems

Idempotency isn't just a nice-to-have; it's fundamental to agent reliability.

When your agent creates a payment request, it might: - Send the request successfully but lose the response (network timeout) - Receive the response but crash before saving the payment ID - Retry due to an internal error, accidentally creating two requests

Without idempotency, your bot could generate duplicate invoices or charge a customer twice.

AgentPay VN prevents this by accepting an idempotency key—a unique identifier you provide. Send the same key twice, and the server returns the same payment request both times:

from agentpay_vn import AgentPayClient

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

# Create a payment request for a course enrollment
# The idempotency_key ensures this request is unique per student per course
idempotency_key = f"course_enrollment_{student_id}_{course_id}_{datetime.now().isoformat()}"

payment = client.create_payment_request(
    amount=299000,  # 299,000 VND for a Python course
    description="Python Advanced Course - Q4 2024",
    merchant_id="your_merchant_id",  # Your VietQR merchant ID
    idempotency_key=idempotency_key,  # Critical: prevents duplicate charges
    metadata={
        "student_id": student_id,
        "course_id": course_id,
        "enrollment_timestamp": datetime.now().isoformat()
    }
)

print(f"Payment ID: {payment.id}")
print(f"Checkout URL: {payment.checkout_url}")
# Output:
# Payment ID: pay_abc123xyz
# Checkout URL: https://vietqr.io/pay/abc123xyz

Key insight: Even if your agent retries this call 10 times with the same idempotency_key, it gets back pay_abc123xyz every time. The checkout URL remains constant. No duplicate charges.

Webhooks: The Agent's Eyes and Ears

An agent can't camp out refreshing a database waiting for settlement. Instead, you configure webhooks—HTTP callbacks that AgentPay VN fires when payment status changes.

Three critical events exist:

  1. payment.created – Payment request was generated (student got the QR code)
  2. payment.confirmed – Student scanned and submitted the QR (confirmation pending)
  3. payment.settled – Funds hit your merchant bank account (final, irreversible)

Your agent should act only after receiving the payment.settled webhook. Before that, the payment is still in limbo.

Setting Up Your Webhook Receiver

Create a simple FastAPI endpoint to handle incoming webhooks:

from fastapi import FastAPI, Request, HTTPException
import hmac
import hashlib
import json
from datetime import datetime

app = FastAPI()
WEBHOOK_SECRET = "your_webhook_secret_from_agentpay_dashboard"  # Store in env

@app.post("/webhooks/agentpay")
async def handle_webhook(request: Request):
    """
    Receive and verify webhooks from AgentPay VN.
    Every webhook is signed with HMAC-SHA256 for security.
    """
    # 1. Read the raw body for signature verification
    raw_body = await request.body()
    signature = request.headers.get("X-AgentPay-Signature")

    # 2. Verify HMAC signature (prevents spoofed webhooks)
    expected_sig = hmac.new(
        WEBHOOK_SECRET.encode(),
        raw_body,
        hashlib.sha256
    ).hexdigest()

    if not hmac.compare_digest(signature, expected_sig):
        raise HTTPException(status_code=401, detail="Invalid signature")

    # 3. Parse the webhook payload
    payload = json.loads(raw_body)
    event_type = payload.get("event")
    payment_data = payload.get("data", {})
    payment_id = payment_data.get("id")

    # 4. Route based on event type
    if event_type == "payment.settled":
        # ** This is where your agent's logic triggers **
        # Fund transfer is confirmed—safe to deliver the course, ship product, etc.
        student_id = payment_data.get("metadata", {}).get("student_id")
        course_id = payment_data.get("metadata", {}).get("course_id")

        # Example: unlock course for student
        unlock_course_for_student(student_id, course_id)

        # Mark payment as processed in your DB
        save_settled_payment(
            payment_id=payment_id,
            student_id=student_id,
            settled_at=datetime.fromisoformat(payment_data.get("settled_at")),
            merchant_account=payment_data.get("merchant_account")  # Proof of settlement
        )

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

    elif event_type == "payment.confirmed":
        # Student submitted payment; waiting for bank to clear it (usually < 1 minute)
        log_payment_status(payment_id, "confirmed")
        return {"status": "acknowledged"}

    return {"status": "ignored"}

Critical pattern: Your agent (or webhook handler) should be idempotent too. If a webhook fires twice due to a network retry, calling unlock_course_for_student() twice should not break anything—use database checks like IF NOT EXISTS or deduplicate by payment_id.

Settlement Confirmation: Proof of Funds

Unlike payment gateways that hold money in escrow, AgentPay VN never holds your funds. The VietQR code points straight at your merchant's bank account. This is faster and more transparent, but you must confirm settlement yourself.

AgentPay VN provides two confirmation methods:

As shown above, payment.settled event includes proof:

{
  "event": "payment.settled",
  "data": {
    "id": "pay_abc123xyz",
    "amount": 299000,
    "merchant_account": "0123456789",
    "settled_at": "2024-11-15T14:32:10Z",
    "bank_reference": "VT241115.1432.ABC123"  // Proof from VietQR network
  }
}

Store bank_reference in your database—it's your audit trail.

Method 2: Polling (Fallback)

If webhooks fail, your agent can poll for updates:

import asyncio

async def wait_for_settlement(payment_id: str, max_wait_seconds: int = 600):
    """
    Poll AgentPay VN every 5 seconds to check if payment settled.
    Timeout after 10 minutes (typical settlement is < 2 minutes).
    """
    start_time = datetime.now()

    while (datetime.now() - start_time).total_seconds() < max_wait_seconds:
        payment = client.await_settlement(payment_id=payment_id)

        if payment.status == "settled":
            print(f"✓ Payment {payment_id} settled at {payment.settled_at}")
            print(f"  Bank reference: {payment.bank_reference}")
            return payment

        elif payment.status == "failed":
            print(f"✗ Payment {payment_id} failed")
            raise Exception(f"Payment failed: {payment.failure_reason}")

        # Still pending; wait before retrying
        await asyncio.sleep(5)

    # Timeout—payment never settled
    raise TimeoutError(f"Payment {payment_id} did not settle within {max_wait_seconds}s")

# Usage
await wait_for_settlement(payment_id="pay_abc123xyz")

Integrating with Claude via MCP Server

AgentPay VN includes an MCP (Model Context Protocol) server that lets Claude directly invoke payment operations. This is how your AI agent can autonomously handle payments.

Configure Claude's MCP

Add to your Claude desktop config (~/.claude/claude.json):

{
  "mcpServers": {
    "agentpay": {
      "command": "python",
      "args": ["-m", "agentpay_mcp"],
      "env": {
        "AGENTPAY_API_KEY": "your_api_key_here",
        "AGENTPAY_MERCHANT_ID": "your_merchant_id",
        "AGENTPAY_WEBHOOK_SECRET": "your_webhook_secret"
      }
    }
  }
}

Now Claude can call tools like: - create_payment_request(amount, description, metadata, idempotency_key) - get_payment_status(payment_id) - await_settlement(payment_id)

Example Claude prompt:

You are a course sales agent. When a customer enrolls:
1. Use create_payment_request to generate a VietQR invoice
2. Share the checkout_url with the customer
3. Use await_settlement to wait for payment (with a 10-minute timeout)
4. Once settled, send a confirmation email with course access details

Customer: "I want to enroll in the Python Advanced course (299,000 VND)"

Claude will orchestrate the entire flow autonomously, handling retries and idempotency.

Real-World Walkthrough: Online Course Enrollment Bot

Let's trace a complete flow:

Minute 0:00 – Student says to bot: "Enroll me in Python Advanced (299k VND)"

  1. Bot creates payment (idempotent): idempotency_key = "enrollment_user_123_course_456_2024-11-15T10:00:00" payment = create_payment_request( amount=299000, idempotency_key=idempotency_key, metadata={"student_id": 123, "course_id": 456} ) // Returns: pay_abc123xyz, checkout_url = https://vietqr.io/pay/abc123xyz

  2. Bot shares QR code with student (shown as image or link)

  3. Student scans QR on their banking app and transfers 299,000 VND

  4. Minute 0:15 – VietQR processes the transaction, AgentPay fires webhook: POST /webhooks/agentpay event: "payment.confirmed" data: { id: "pay_abc123xyz", status: "confirmed" }

  5. Minute 0:45 – Bank clears funds to course platform's merchant account, AgentPay fires: POST /webhooks/agentpay event: "payment.settled" data: { id: "pay_abc123xyz", status: "settled", settled_at: "2024-11-15T10:00:45Z", bank_reference: "VT241115.1000.ABC123" }

  6. Bot's webhook handler: - Verifies HMAC signature ✓ - Extracts student_id=123, course_id=456 - Unlocks course access in database - Sends email: "Welcome! Your course is ready: [access link]"

  7. Minute 0:46 – Student receives email and clicks link; course dashboard loads ✓

Total time: ~46 seconds from QR scan to course access.

Do's and Don'ts: Common Pitfalls

Do Don't
Always provide an idempotency_key when creating payments Retry without changing the idempotency_key; AgentPay will return the same payment
Store bank_reference from settled webhooks for audit trails Trust only the payment.created event; funds aren't confirmed yet
Verify webhook HMAC signatures with hmac.compare_digest() Use string comparison (==) for signatures; vulnerable to timing attacks
Implement webhook retry logic on your server (e.g., return 5xx if processing fails) Fire fulfillment logic before receiving payment.settled webhook
Set webhook timeout to 30+ seconds and retry for 24 hours Assume a payment is settled just because the QR was scanned
Use await_settlement() as a fallback, with a max_wait timeout Poll indefinitely without a timeout; could hang your agent forever

FAQ: Idempotency, Webhooks, and Settlement

Q: What if my webhook handler crashes after verifying the signature but before unlocking the course?

A: AgentPay will retry the webhook for 24 hours. Your handler should be idempotent—e.g., use INSERT ... ON CONFLICT DO NOTHING in SQL so the second retry does no harm. Alternatively, track processed webhook IDs in a separate table.

Q: How long does settlement actually take?

A: Typically 30 seconds to 2 minutes after the student submits payment. VietQR operates on real-time bank transfers. The payment.confirmed event fires immediately; payment.settled follows once your bank confirms the incoming transfer.

Q: Can I use the same idempotency_key for multiple customers?

A: No. Each payment should have a unique key. A good pattern is f"{business_logic}_{unique_id}_{timestamp}", e.g., course_enrollment_user_123_2024-11-15T10:00:00. This ensures each customer's payment is distinct.

Q: What if AgentPay's webhook endpoint is down?

A: AgentPay will retry every 5 minutes for 24 hours. You'll eventually receive the webhook. In the meantime, your agent can use await_settlement() as a fallback to poll AgentPay directly.

Q: How does AgentPay verify it's my merchant account receiving funds?

A: During onboarding, you register your bank account with AgentPay. Every VietQR code is tied to your merchant ID and directs payments to that account. AgentPay doesn't hold or route funds—it just confirms the transfer happened via bank feeds.

Key Takeaways

Get Started Today

Ready to build AI agents that collect payments reliably? Here's what you need:

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

  2. Get your API key from the AgentPay VN dashboard and set AGENTPAY_API_KEY env var.

  3. Review the full documentation at https://agentpay.servicesai.vn/v1/docs for webhook setup, MCP integration, and production best practices.

  4. Check out the source code on GitHub for examples and contribution guidelines.

Your AI agent can now collect payments with the reliability of a professional payment processor—autonomously, securely, and without holding customer funds. That's the AgentPay VN difference.

Get started →

← All posts