FloPayFloPay
GatewaysPayPal

Direct PayPal

Provision a direct PayPal gateway and render PayPal through the PayPal JS SDK. Works inside Facebook, Meta, and Instagram in-app browsers.

Direct PayPal

Direct PayPal is FloPay's first-class PayPal integration. PayPal renders through the PayPal JS SDK directly — not through Stripe's ExpressCheckoutElement. It works inside Facebook, Meta, and Instagram in-app browsers, where the Stripe-rendered PayPal fallback does not.

New to PayPal in FloPay? Start with the PayPal overview to understand how FloPay picks between direct PayPal and the Stripe-rendered fallback.

When Direct PayPal Renders

The SDK reads the gateways map on the checkout session response and routes automatically:

  • session.gateways.paypal present → direct PayPal renders (the PayPal JS SDK loads with the gateway's client_id and environment).
  • session.gateways.paypal null or missing → the Stripe-rendered PayPal fallback renders instead.

PayPal never renders twice. Selection is per checkout session.

session.gateways.paypal is populated when the client has an active provider_type=paypal gateway record. Direct PayPal is opt-in per client.

API Shape

The session response uses the gateways map:

{
  "id": "sess_abc123",
  "amount": 4999,
  "currency": "usd",
  "gateways": {
    "stripe": {
      "publishableKey": "pk_live_xxx",
      "environment": "production"
    },
    "paypal": {
      "publishableKey": "AY...PayPalClientId",
      "environment": "production"
    }
  }
}

For the field-level reference see CheckoutGatewaysDto and CheckoutGatewayDto.

Breaking change. The previous top-level gateway: BillingProvider field and gatewayData: { publishableKey, paypalPublishableKey } shape are gone. If you read the session response directly from your backend, update your reader before this ships. The SDK already consumes the new shape transparently.

Setup

Direct PayPal is opt-in per client. Provisioning takes three steps: create the PayPal app, capture the credentials, and populate a gateway record.

1. Create a PayPal App

In the PayPal developer dashboard, create a REST API app for each environment you need:

  • Sandbox app — for stage testing. Captures client_id and client_secret against the PayPal sandbox.
  • Live app — for production. Captures client_id and client_secret against PayPal live.

Enable the following features on each app:

  • Accept payments (Orders v2)
  • Subscriptions
  • Vault (for upsell / saved-payment reuse)
  • Webhooks

2. Register the Webhook

On the same PayPal app, register a webhook with FloPay's inbound URL for that environment and subscribe to the 15 PayPal events FloPay handles:

  • CHECKOUT.ORDER.APPROVED
  • PAYMENT.CAPTURE.COMPLETED
  • PAYMENT.CAPTURE.DENIED
  • PAYMENT.CAPTURE.PENDING
  • PAYMENT.CAPTURE.REFUNDED
  • PAYMENT.CAPTURE.REVERSED
  • BILLING.SUBSCRIPTION.CREATED
  • BILLING.SUBSCRIPTION.ACTIVATED
  • BILLING.SUBSCRIPTION.UPDATED
  • BILLING.SUBSCRIPTION.CANCELLED
  • BILLING.SUBSCRIPTION.SUSPENDED
  • BILLING.SUBSCRIPTION.EXPIRED
  • BILLING.SUBSCRIPTION.PAYMENT.FAILED
  • VAULT.PAYMENT-TOKEN.CREATED
  • VAULT.PAYMENT-TOKEN.DELETED

PayPal returns a webhook id after registration. Capture it — FloPay uses it for signature verification.

See PayPal webhook events for the inbound catalog and how each event maps to Flo's outbound subscription.* / item.* / invoice.* webhooks.

3. Populate a Gateway Record

Create one gateway record per environment, using the PayPal credentials from steps 1 and 2:

ColumnValue
codeA short identifier for the gateway (e.g. paypal-live, paypal-stage).
provider_typepaypal
publishable_keyThe PayPal app's client_id. The SDK sends this to the PayPal JS SDK as the client-id query parameter.
secret_keyThe PayPal app's client_secret. Used server-side to call PayPal REST endpoints.
webhook_signing_secretThe PayPal webhook id captured in step 2.
environmentstage for the sandbox app, production for the live app.

webhook_signing_secret stores the PayPal webhook id, not an HMAC secret. The column name is provider-neutral; for PayPal, FloPay passes it to PayPal's hosted /v1/notifications/verify-webhook-signature endpoint to verify each delivery. Local certificate-based verification is deferred.

Once the gateway record is active, the next checkout session created for that client returns gateways.paypal populated, and the SDK switches to direct PayPal automatically.

SDK Integration

Most integrations need no change. FloPayCheckout and SplitCardForm consume session.gateways and switch paths automatically:

import { FloPayCheckout } from '@flopay/react';

function CheckoutPage({ sessionId }: { sessionId: string }) {
  return (
    <FloPayCheckout
      sessionId={sessionId}
      onComplete={(result) => {
        if (result.status === 'succeeded') window.location.href = '/success';
      }}
      onError={(err) => console.error(err.message)}
    />
  );
}

When the session's gateways.paypal is present, the SDK loads the PayPal JS SDK scoped to that gateway's publishableKey and environment. Each PayPal gateway gets its own loader — sandbox and live coexist without leaking credentials across environments.

Lifecycle

One-Time Purchases

Direct PayPal one-time purchases go through the PayPal Orders v2 API:

  1. Buyer clicks the PayPal button (rendered by the PayPal JS SDK).
  2. FloPay creates a PayPal order with the line items and totals.
  3. PayPal opens the approval window (or in-app browser flow).
  4. On approval, FloPay captures the order against PayPal and processes the result against the FloPay session.
  5. PAYMENT.CAPTURE.COMPLETED arrives via webhook and maps to item.purchased on the outbound Flo webhook.

Subscriptions

Subscriptions use PayPal's Subscriptions API. PayPal — not Flo — drives the recurring billing cycle.

EventPayPal APIFlo outbound webhook
SignupCreate + approve a PayPal subscriptionsubscription.created, subscription.renewed (on the first cycle paid)
Recurring auto-chargePayPal's native renewal cyclesubscription.renewed
PausePOST /v1/billing/subscriptions/{id}/suspendsubscription.paused
ResumePOST /v1/billing/subscriptions/{id}/activatesubscription.resumed
CancelPOST /v1/billing/subscriptions/{id}/cancelsubscription.cancelled
Expire(PayPal lifecycle end)subscription.expired
Failed payment(PayPal recurring failure)invoice.overdue

Because PayPal drives the recurring cycle natively, Flo does not issue off-session retries on PayPal subscriptions. PayPal's own dunning rules apply — BILLING.SUBSCRIPTION.PAYMENT.FAILED arrives only when PayPal has exhausted its retry window.

Refunds

Refund a direct PayPal capture against its PayPal capture id. FloPay forwards the refund via PayPal's POST /v2/payments/captures/{capture_id}/refund endpoint. The resulting PAYMENT.CAPTURE.REFUNDED maps to item.refunded on the outbound Flo webhook.

Vaulted Token Reuse (Upsells)

When a buyer completes their first direct PayPal purchase, PayPal returns a vaulted payment token. FloPay persists it on user_payment_method with type='paypal', keyed by FloPay's user id.

For upsell flows you can charge the saved PayPal token without re-presenting the PayPal button. The flow is:

  1. On the upsell page, call the existing FloPay automatic-payment-button or saved-payment-method API — the same surface used for saved cards.
  2. FloPay locates the user_payment_method of type='paypal' for the user, and uses the vaulted token to charge PayPal directly.
  3. VAULT.PAYMENT-TOKEN.CREATED arrives at the first checkout; VAULT.PAYMENT-TOKEN.DELETED arrives if the buyer later removes the saved method from their PayPal account.

Sandbox Testing Checklist

Provision a paypal-stage gateway record (environment='stage') wired to your PayPal sandbox app, then walk this checklist before promoting to production:

  • One-time purchase: complete a sandbox PayPal payment; confirm item.purchased arrives on the client webhook.
  • Subscription signup: create a subscription via direct PayPal; confirm subscription.created and (on the first renewal) subscription.renewed.
  • Recurring renewal: use the PayPal sandbox's accelerated billing cycle to trigger a renewal; confirm subscription.renewed on the next cycle.
  • Pause / resume: pause the subscription; confirm subscription.paused. Resume; confirm subscription.resumed.
  • Cancel: cancel the subscription; confirm subscription.cancelled.
  • Refund: refund the first capture against the PayPal capture id; confirm item.refunded.
  • Vault upsell round-trip: complete a first purchase, then run an upsell using the saved PayPal token without re-rendering the PayPal button. Confirm VAULT.PAYMENT-TOKEN.CREATED arrived on the first purchase and the upsell charge succeeded.
  • Meta in-app browser: open the checkout from a Facebook / Instagram ad on mobile and confirm the PayPal button renders and the flow returns to your success URL.
  • Failed renewal: trigger a sandbox renewal failure; confirm invoice.overdue arrives.
  • Read the PayPal sandbox event timeline: confirm each inbound PayPal event for the runs above appears, and that FloPay processed it (no retries pending against your gateway record's environment='stage').

Migration Notes

  • Existing Stripe-rendered PayPal clients stay on the Stripe-rendered path with no change. gateways.paypal is null for their sessions, so the SDK renders the Stripe-rendered PayPal fallback.
  • Opting a client into direct PayPal is a per-client provisioning step — create a provider_type=paypal gateway record (see Setup). Sessions created after that point return gateways.paypal populated and the SDK switches automatically.
  • The breaking API response change (removal of gateway and gatewayData, addition of gateways) ships at the same time as direct PayPal. SDK consumers are unaffected because the SDK handles the new shape. Direct API consumers must update their reader before this ships.
  • Rolling back is a matter of deactivating the provider_type=paypal gateway record for that client. New sessions drop gateways.paypal and the SDK falls back to the Stripe path immediately.

On this page