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:
- Backend Engineer Pillar Guide (Python, Django, FastAPI, Go & PostgreSQL)
- Cloud Engineer Pillar Guide (AWS, GCP, Terraform & FinOps)
- GCP Professional Cloud Architect Guide & GCP Professional Data Engineer Guide
- AI Application Engineer Guide & Full Stack Engineer Guide
- Book the $100 USD / Rp 1,000,000 1-Day Programming Service