Welcome to the Jose Madrid Salsa developer docs — explore features, APIs, and deployment guides.
Jose Madrid SalsaJMS Docs

PayPal Integration

PayPal payment adapter using the Orders API with redirect-based authorization, cached OAuth tokens, and Venmo support

PayPal Integration

PayPal handles wallet-based payments and Venmo through the PayPal Orders API. It uses a redirect/popup-based authorization flow rather than client-side card collection.

Architecture

PayPalAdapter

The PayPalAdapter class implements PaymentProviderAdapter using the @paypal/checkout-server-sdk.

Conditional Registration

PayPal is only registered when both credentials are available:

if (process.env.PAYPAL_CLIENT_ID && process.env.PAYPAL_CLIENT_SECRET) {
  registerProvider(new PayPalAdapter())
}

Client Initialization

The adapter lazily initializes a singleton PayPalHttpClient, choosing sandbox or live environment based on PAYPAL_SANDBOX:

const environment = config.sandbox
  ? new paypal.core.SandboxEnvironment(config.clientId, config.clientSecret)
  : new paypal.core.LiveEnvironment(config.clientId, config.clientSecret)

OAuth Token Caching

PayPal OAuth2 tokens last approximately 9 hours. The adapter caches the token and refreshes 5 minutes before expiry to avoid races during webhook processing. This saves 200-500ms per webhook call.

Payment Flow

  1. createPayment() creates a PayPal Order via the Orders API
  2. Returns an approvalUrl that the customer is redirected to
  3. Customer approves payment in PayPal
  4. PayPal redirects back to PAYPAL_RETURN_URL
  5. confirmPayment() captures the approved order

API Timeout

All PayPal API calls have a 15-second timeout to prevent checkout from hanging:

const PAYPAL_API_TIMEOUT_MS = 15_000

Client-Side Components

PayPal components are lazy-loaded to avoid bundling the PayPal SDK (~100KB+) when the user selects card payment:

const PayPalProvider = dynamic(
  () => import('@/components/checkout/PayPalProvider'),
  { ssr: false }
)
const PayPalButton = dynamic(
  () => import('@/components/checkout/PayPalButton'),
  { ssr: false }
)
const VenmoButton = dynamic(
  () => import('@/components/checkout/VenmoButton'),
  { ssr: false }
)

Environment Variables

VariableDescription
PAYPAL_CLIENT_IDPayPal app client ID
PAYPAL_CLIENT_SECRETPayPal app secret
PAYPAL_SANDBOX"true" (default) or "false" for production
PAYPAL_RETURN_URLRedirect after approval (defaults to /checkout/paypal/return)
PAYPAL_CANCEL_URLRedirect on cancel (defaults to /checkout/paypal/cancel)

PayPal and Venmo share the same PayPal Orders API backend. The checkout component renders separate buttons for each, but both route through the PayPalAdapter.

How is this guide?

Edit on GitHub

Last updated on

On this page