Agent-Native Payments: Idempotency, Webhooks & Settlement

2026-09-03 · AgentPay VN

ai-agentspaymentsvietqrpythonwebhooks

The Problem: Why Your AI Agent's Payment Flow Breaks

You've built a smart chatbot that sells online courses. A customer asks to enroll, your agent creates a payment request, and generates a QR code. But here's where it gets messy:

This isn't hypothetical. When AI agents handle payments, traditional payment flows break down. Agents don't think like humans—they retry, fork execution paths, and operate at scale. You need payment infrastructure built for agent behavior: idempotency, reliable webhooks, and transparent settlement.

AgentPay VN solves this with an open-source Python SDK and MCP server that lets your agent collect payments directly into your merchant bank account via VietQR. No middleman holding your money. No payment gateway markup. Just your agent, your customer, and your bank.

Understanding Idempotency in Agent Payments

Idempotency is the foundation of reliable agent payments. It means: calling the same payment request twice produces the same result, not two charges.

Here's why agents need this:

  1. Retry Logic: Agents automatically retry failed operations. Without idempotency, a network timeout becomes a duplicate charge.
  2. Parallel Execution: Modern AI frameworks spawn multiple agent threads. Both might attempt payment creation simultaneously.
  3. Uncertainty on Failure: When a request times out, the agent doesn't know if it succeeded or failed—so it retries.

AgentPay VN handles idempotency through request IDs. Every create_payment_request call requires an idempotency_key—a unique identifier for that transaction. If you call it twice with the same key, you get the same payment request back. No duplicate charge.

How to Implement Idempotency

The key is making your idempotency key stable and deterministic:

from agentpay_vn import PaymentClient
import hashlib

# Initialize the client
client = PaymentClient(api_key="your_api_key")

# For a course enrollment, use customer_id + course_id + timestamp
customer_id = "user_12345"
course_id = "python_basics_101"
purchase_attempt = "2025-01-15T14:30:00"

# Generate a stable idempotency key
idempotency_key = hashlib.sha256(
    f"{customer_id}:{course_id}:{purchase_attempt}".encode()
).hexdigest()

# Create payment request with idempotency
payment = client.create_payment_request(
    idempotency_key=idempotency_key,  # This makes it safe to retry
    amount=299000,  # Vietnamese Dong
    merchant_account="0123456789",  # Your bank account
    description="Python Basics Course",
    customer_email="student@example.com",
    order_id=f"course_{course_id}_{customer_id}"
)

print(f"Checkout URL: {payment['checkout_url']}")
print(f"Request ID: {payment['request_id']}")

Line-by-line explanation: - idempotency_key: A SHA256 hash combining customer, course, and timestamp ensures uniqueness and stability. - amount: Always in Vietnamese Dong (VND). 299,000 VND ≈ $12 USD. - merchant_account: Your bank account number (AgentPay never holds this money). - If you call this function again with the same idempotency_key, you get the exact same checkout_url and request_id—no new charge created.

Webhooks: Real-Time Settlement Confirmation

Once your customer scans the QR code and pays, the bank processes the transaction. But how does your agent know? Webhooks.

AgentPay VN sends a webhook to your server when settlement is confirmed—meaning the money cleared and landed in your merchant account. This is not a pending notification; it's a "money is here" confirmation.

Setting Up Webhook Handling

from flask import Flask, request, jsonify
from agentpay_vn import verify_webhook_signature

app = Flask(__name__)

@app.route("/webhooks/payment-settled", methods=["POST"])
def handle_payment_webhook():
    """
    Webhook endpoint called when AgentPay confirms settlement.
    The money is already in your bank account.
    """

    payload = request.json
    signature = request.headers.get("X-AgentPay-Signature")
    webhook_secret = "your_webhook_secret"

    # Always verify the signature to prevent spoofing
    if not verify_webhook_signature(payload, signature, webhook_secret):
        return jsonify({"error": "Invalid signature"}), 403

    # Extract payment details
    request_id = payload["request_id"]
    amount = payload["amount"]  # VND
    status = payload["status"]  # "settled", "failed", etc.
    timestamp = payload["settled_at"]

    if status == "settled":
        # Money confirmed in your bank account
        print(f"Payment settled: {amount} VND (ID: {request_id})")

        # Now do your business logic:
        # - Enroll student in course
        # - Send confirmation email
        # - Log transaction
        enroll_student(request_id, amount)
        send_confirmation_email(request_id)

    elif status == "failed":
        print(f"Payment failed: {request_id}")
        # Handle refund or retry logic

    # Always return 200 OK to confirm receipt
    # AgentPay will retry if it gets 4xx or 5xx
    return jsonify({"received": True}), 200

def enroll_student(request_id, amount):
    """Example: enroll student after payment settled."""
    # Query your database to find the student by request_id
    # Update enrollment status
    pass

def send_confirmation_email(request_id):
    """Send access credentials to student."""
    pass

if __name__ == "__main__":
    app.run(port=5000)

Key points: - verify_webhook_signature(): Essential for security. Prevents attackers from spoofing payment confirmations. - status == "settled": This means the money is already in your bank account. No pending, no risk of reversal. - Always return 200 OK even if your business logic fails—AgentPay will retry if you return an error.

MCP Server: Integrating Payments into Claude

If you're using Claude (or another AI agent), the MCP (Model Context Protocol) server lets your agent call payment functions directly.

Configure Claude with AgentPay MCP

Add this to your Claude configuration (typically ~/.claude/config.json or your environment):

{
  "mcpServers": {
    "agentpay": {
      "command": "agentpay-mcp",
      "env": {
        "AGENTPAY_API_KEY": "your_api_key_here",
        "AGENTPAY_WEBHOOK_SECRET": "your_webhook_secret"
      }
    }
  }
}

Now Claude can call these tools directly in its reasoning:

User: "I want to buy the advanced Python course for 599,000 VND."

Claude: [reasoning] The user wants to purchase. I'll create a payment request.

Tool Call: create_payment_request
- amount: 599000
- description: "Advanced Python Course - Full Lifetime Access"
- customer_email: user@example.com

Tool Result: {"checkout_url": "vietqr://...", "request_id": "req_abc123"}

Claude: "Your course access is almost ready! Scan this QR code to complete payment: [QR image]. I'll notify you as soon as your payment settles."

The agent doesn't wait for settlement—it sends the QR, then monitors via webhooks.

Real-World Walkthrough: Online Course Chatbot

Let's build a complete example: a chatbot that sells online courses.

Scenario: Customer asks to buy a course. The bot: 1. Creates a payment request (idempotent). 2. Sends a checkout link with QR code. 3. Webhook confirms settlement → bot enrolls student. 4. Student gets instant access.

from agentpay_vn import PaymentClient, await_settlement
import hashlib
import time

client = PaymentClient(api_key="your_api_key")

def sell_course_to_customer(customer_email, customer_name, course_id, course_price):
    """
    Complete payment flow for course purchase.
    """

    # Step 1: Create idempotency key
    idempotency_key = hashlib.sha256(
        f"{customer_email}:{course_id}:{int(time.time())}".encode()
    ).hexdigest()

    # Step 2: Create payment request
    print(f"Creating payment for {customer_name}...")
    payment = client.create_payment_request(
        idempotency_key=idempotency_key,
        amount=course_price,  # in VND
        merchant_account="0123456789",
        description=f"Course: {course_id} - {customer_name}",
        customer_email=customer_email,
        order_id=f"course_{course_id}_{int(time.time())}"
    )

    request_id = payment["request_id"]
    checkout_url = payment["checkout_url"]

    print(f"✓ Payment request created (ID: {request_id})")
    print(f"✓ Checkout URL: {checkout_url}")

    # Step 3: Wait for settlement (with timeout)
    print(f"Waiting for settlement confirmation...")

    settlement = await_settlement(
        request_id=request_id,
        timeout_seconds=300  # Wait max 5 minutes
    )

    if settlement["status"] == "settled":
        print(f"✓ PAYMENT SETTLED: {settlement['amount']} VND")
        print(f"✓ Settlement time: {settlement['settled_at']}")

        # Step 4: Enroll student in course
        enroll_student(customer_email, course_id)
        send_course_access_email(customer_email, customer_name, course_id)

        return {"success": True, "request_id": request_id, "course_id": course_id}

    else:
        print(f"✗ PAYMENT FAILED: {settlement['status']}")
        return {"success": False, "request_id": request_id, "reason": settlement["status"]}

# Usage
result = sell_course_to_customer(
    customer_email="student@example.com",
    customer_name="Nguyen Van A",
    course_id="python_advanced_2025",
    course_price=599000  # 599,000 VND
)

print(f"\nResult: {result}")

This flow is agent-safe: - Idempotent: calling it twice with same customer+course+time → same QR - Reliable: webhook confirms money actually landed - Transparent: you see settlement status in real-time

Do's and Don'ts for Agent Payments

Do Don't
Always use idempotency keys for every payment Assume a timeout means failure (always check status)
Verify webhook signatures Trust webhooks without signature verification
Return 200 OK from webhook handlers even on error Return 5xx if your business logic fails (causes retries)
Store request_id for every transaction Create duplicate payment requests to "retry" manually
Use await_settlement() with a timeout Block indefinitely waiting for settlement
Log webhook events and settlement confirmations Rely only on agent memory (logs are immutable proof)

Advanced Tips

Retry Strategy with Exponential Backoff

If await_settlement() times out, don't immediately give up:

import time

def robust_await_settlement(request_id, max_retries=3):
    """
    Retry settlement check with exponential backoff.
    """
    backoff = 1  # Start at 1 second

    for attempt in range(max_retries):
        try:
            settlement = client.await_settlement(request_id, timeout_seconds=60)
            if settlement["status"] == "settled":
                return settlement
        except TimeoutError:
            print(f"Attempt {attempt + 1} timed out, retrying...")
            time.sleep(backoff)
            backoff *= 2  # Exponential backoff: 1s → 2s → 4s

    # After retries exhausted, return last known status
    return client.get_payment_status(request_id)

Handling Concurrent Payments

If multiple agents try to pay for the same thing simultaneously:

# Each agent generates the same idempotency key
# (e.g., customer_id + course_id + purchase_date)
# First agent creates payment → all agents get same QR
# Only one charge occurs, even with parallel requests

FAQ

Q: Does AgentPay hold my money? A: No. The QR code points directly at your merchant bank account. Settlement is confirmed when the money lands in your bank account, not AgentPay's. We're open-source MIT license—inspect the code yourself at https://github.com/phuocdu/agentpay-vn.

Q: What if the customer doesn't scan the QR within 24 hours? A: Payment requests expire after 24 hours. The customer can still scan, but the transaction won't settle after expiry. You can create a new payment request (with a different idempotency key) and resend the QR.

Q: Can I integrate this with Telegram/Discord bots? A: Yes. The SDK works with any Python bot framework. Generate the QR code URL and send it to the chat. Webhook confirms settlement and you post a follow-up message to the customer.

Q: What happens if my webhook endpoint is unreachable? A: AgentPay retries the webhook 5 times over 24 hours with exponential backoff. Always expose your webhook to the internet and monitor it. For testing locally, use ngrok or similar tunneling.

Key Takeaways

Getting Started

Ready to build agent-native payments?

Install AgentPay VN:

pip install agentpay-vn

Review full documentation: https://agentpay.servicesai.vn/v1/docs

Explore the open-source code: https://github.com/phuocdu/agentpay-vn

Next steps: 1. Get an API key from the docs. 2. Test create_payment_request() in your local environment. 3. Deploy a webhook handler (Flask, FastAPI, or your framework). 4. Configure the MCP server if using Claude. 5. Run a test transaction end-to-end (use sandbox mode if available).

Your AI agents are ready to monetize. Build with confidence, knowing payments won't ghost you.

Get started →

← All posts