PayPal Integration¶
Environment Variables¶
| Variable | Required | Description |
|---|---|---|
PAYPAL_CLIENT_ID |
Yes | From PayPal developer dashboard |
PAYPAL_CLIENT_SECRET |
Yes | From PayPal developer dashboard |
PAYPAL_WEBHOOK_ID |
Yes | Webhook ID for signature verification |
PAYPAL_MODE |
No | sandbox (default) or live |
DASHBOARD_URL |
Yes | e.g. https://dashboard.example.com — used for return/cancel redirect URLs |
Developer Account Setup¶
- Go to developer.paypal.com → Apps & Credentials
- Select Sandbox tab → Create App
- Copy
PAYPAL_CLIENT_IDandPAYPAL_CLIENT_SECRET - Scroll down → Webhooks → Add webhook URL:
https://your-domain.com/webhooks/paypal - Subscribe to events:
PAYMENT.CAPTURE.COMPLETED,PAYMENT.CAPTURE.REFUNDED - Save → copy the Webhook ID →
PAYPAL_WEBHOOK_ID
Webhook Endpoint¶
No /api/v1 prefix. Handles:
- PAYMENT.CAPTURE.COMPLETED — marks invoice paid, triggers provisioning
- PAYMENT.CAPTURE.REFUNDED — marks invoice refunded, cancels order and tears down K8s resources
Sandbox Testing¶
Sandbox buyer/merchant accounts are at developer.paypal.com → Testing → Sandbox Accounts. - Personal account = buyer - Business account = merchant
Sandbox Geo-Restriction
PayPal sandbox blocks certain ISPs/regions (COMPLIANCE_VIOLATION). This is a sandbox-only quirk. Use a VPN (US or UK) only for sandbox testing. Live PayPal is not affected.
Data Flow¶
invoice.external_id= PayPal Order ID, stored at checkoutPAYMENT.CAPTURE.REFUNDEDwebhook carriesresource.links[rel=order].href— last path segment is the Order ID, matched againstinvoice.external_id- Return URL after payment:
/orders/{order_id}/billing?payment=success - Cancel URL after payment:
/orders/{order_id}/billing?payment=cancelled
Idempotency¶
PAYMENT.CAPTURE.COMPLETEDon an already-paid invoice → 200, ignoredPAYMENT.CAPTURE.REFUNDEDon an already-refunded invoice → 200, ignored- Both always return 200 to prevent PayPal retries