Idempotent Multi-Currency Checkout Architecture in Django & PostgreSQL: Orders API v2 Auto-Capture, Webhook Signature Verification & QRIS

Production FinTech & SaaS Pattern: Global software platforms serving both international cardholders (USD) and Southeast Asian domestic buyers (IDR via QRIS & Bank Virtual Accounts) frequently suffer from three failure modes: (1) uncaptured APPROVED orders when buyers close tabs mid-redirect, (2) duplicate fulfillment notifications when return-URL hydration races against asynchronous webhooks, and (3) forged webhook POSTs bypassing naive header checks. Below is the battle-tested Python, Django, and PostgreSQL architecture we use in production on Google Cloud Run.

1. Two-Phase Order Lifecycle: Return-URL Auto-Capture + Asynchronous Webhook Recovery

Unlike single-stephosted checkouts that capture funds before redirecting, two-step order gateways transition an order from CREATED / PAYER_ACTION_REQUIRED to APPROVED when the buyer authorizes payment, and only settle funds after your backend invokes POST /v2/checkout/orders/{order_id}/capture. Relying solely on the browser return URL loses orders if mobile networks drop during redirect; relying solely on webhooks leaves returning buyers staring at a spinner. Combining both behind an idempotent PostgreSQL row lock solves both:

import logging
import requests
from django.db import transaction
from django.utils import timezone

log = logging.getLogger(__name__)


def capture_and_settle_usd_order(order_uuid: str, gateway_order_id: str, access_token: str, api_base: str):
    """
    Idempotently verifies custom_id ownership, captures APPROVED orders, handles
    HTTP 422 ORDER_ALREADY_CAPTURED races, and locks the PostgreSQL row once.
    """
    resp = requests.get(
        f"{api_base}/v2/checkout/orders/{gateway_order_id}",
        headers={"Authorization": f"Bearer {access_token}", "Accept": "application/json"},
        timeout=12,
    )
    resp.raise_for_status()
    order_data = resp.json()

    units = order_data.get("purchase_units") or []
    if not any((u.get("custom_id") == str(order_uuid)) for u in units):
        raise ValueError(f"Order {gateway_order_id} custom_id mismatch for {order_uuid}")

    status_upper = (order_data.get("status") or "").upper()
    if status_upper == "APPROVED":
        cap = requests.post(
            f"{api_base}/v2/checkout/orders/{gateway_order_id}/capture",
            headers={
                "Authorization": f"Bearer {access_token}",
                "Content-Type": "application/json",
                "PayPal-Request-Id": f"capture-{order_uuid}",
            },
            json={},
            timeout=15,
        )
        if cap.status_code in (200, 201):
            order_data = cap.json()
            status_upper = (order_data.get("status") or "").upper()
        elif cap.status_code == 422 and "ORDER_ALREADY_CAPTURED" in cap.text:
            order_data = requests.get(
                f"{api_base}/v2/checkout/orders/{gateway_order_id}",
                headers={"Authorization": f"Bearer {access_token}"},
                timeout=12,
            ).json()
            status_upper = (order_data.get("status") or "").upper()

    if status_upper != "COMPLETED":
        return False

    capture_id = ""
    for u in order_data.get("purchase_units") or []:
        for c in ((u.get("payments") or {}).get("captures")) or []:
            if (c.get("status") or "").upper() == "COMPLETED":
                capture_id = c.get("id") or ""
                break

    from core.models import OneDayServiceRequest
    with transaction.atomic():
        locked = OneDayServiceRequest.objects.select_for_update().get(uuid=order_uuid)
        if locked.payment_status == OneDayServiceRequest.PAYMENT_PAID:
            return True  # Already settled by concurrent webhook or SSR return!
        locked.payment_status = OneDayServiceRequest.PAYMENT_PAID
        locked.paypal_order_id = gateway_order_id
        locked.paypal_capture_id = capture_id
        locked.paid_at = timezone.now()
        locked.save(update_fields=["payment_status", "paypal_order_id", "paypal_capture_id", "paid_at", "modified_at"])
    return True

2. Cryptographic Webhook Verification & Dynamic Sandbox Allowlisting

Never trust unverified webhook JSON payloads or allow arbitrary callers to toggle sandbox credentials via query parameters. Verify asymmetric SHA256withRSA transmission headers against the gateway verification endpoint, and gate sandbox routing strictly on server-side verified user identities stored in Cloud Firestore or PostgreSQL:

def verify_gateway_transmission(raw_body: bytes, meta: dict, webhook_id: str, access_token: str, api_base: str) -> bool:
    required = {
        "transmission_id": meta.get("HTTP_PAYPAL_TRANSMISSION_ID", ""),
        "transmission_time": meta.get("HTTP_PAYPAL_TRANSMISSION_TIME", ""),
        "transmission_sig": meta.get("HTTP_PAYPAL_TRANSMISSION_SIG", ""),
        "cert_url": meta.get("HTTP_PAYPAL_CERT_URL", ""),
        "auth_algo": meta.get("HTTP_PAYPAL_AUTH_ALGO", ""),
    }
    if not webhook_id or not all(required.values()):
        return False
    res = requests.post(
        f"{api_base}/v1/notifications/verify-webhook-signature",
        headers={"Authorization": f"Bearer {access_token}", "Content-Type": "application/json"},
        json={**required, "webhook_id": webhook_id, "webhook_event": __import__("json").loads(raw_body.decode("utf-8"))},
        timeout=10,
    )
    return res.status_code == 200 and (res.json().get("verification_status") or "").upper() == "SUCCESS"

3. Zero-Downtime Additive Schema Rollouts on Google Cloud Run & Cloud SQL

When migrating live payment providers behind Google Cloud Run auto-scaling instances, adding NOT NULL DEFAULT '' columns in PostgreSQL 11+ updates catalog metadata instantaneously without table rewrites or breaking in-flight requests on older container revisions.

4. Hire a Senior Backend, Cloud, Full Stack & AI Application Engineer

Whether you need an idempotent multi-currency checkout engine, a Cloud Run / GKE architecture, BigQuery streaming analytics, or production AI agents, explore our engineering pillar hubs or book a 1-day sprint:


About the Author — Hire Aldian Fazrihady (Cloud Engineer, GCP Professional Cloud Architect, GCP Professional Data Engineer, Backend, AI & Full Stack Engineer)

Aldian Fazrihady is a Top-Rated Senior Cloud Engineer, GCP Professional Cloud Architect (Google Cloud Certified), GCP Professional Data Engineer (Google Cloud Certified), Backend Engineer (Python, Django, FastAPI, Go, PostgreSQL), AI Application Engineer (LLMs, RAG, MCP, Google ADK, LangGraph), and Full Stack Engineer (React, Next.js, TypeScript) with 15+ years of production software engineering experience.

Hire Aldian Fazrihady on Upwork | Book 1-Day Programming Service ($100 USD / Rp 1.000.000) | View Live AI Portfolio | Send a Direct Message

Related Engineering Articles

Engineering Role Pillar Guides & Category Hubs