Skip to content

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

  1. Go to developer.paypal.com → Apps & Credentials
  2. Select Sandbox tab → Create App
  3. Copy PAYPAL_CLIENT_ID and PAYPAL_CLIENT_SECRET
  4. Scroll down → Webhooks → Add webhook URL: https://your-domain.com/webhooks/paypal
  5. Subscribe to events: PAYMENT.CAPTURE.COMPLETED, PAYMENT.CAPTURE.REFUNDED
  6. Save → copy the Webhook IDPAYPAL_WEBHOOK_ID

Webhook Endpoint

POST /webhooks/paypal

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 checkout
  • PAYMENT.CAPTURE.REFUNDED webhook carries resource.links[rel=order].href — last path segment is the Order ID, matched against invoice.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.COMPLETED on an already-paid invoice → 200, ignored
  • PAYMENT.CAPTURE.REFUNDED on an already-refunded invoice → 200, ignored
  • Both always return 200 to prevent PayPal retries