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.paypalpresent → direct PayPal renders (the PayPal JS SDK loads with the gateway'sclient_idandenvironment).session.gateways.paypalnullor 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_idandclient_secretagainst the PayPal sandbox. - Live app — for production. Captures
client_idandclient_secretagainst 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.APPROVEDPAYMENT.CAPTURE.COMPLETEDPAYMENT.CAPTURE.DENIEDPAYMENT.CAPTURE.PENDINGPAYMENT.CAPTURE.REFUNDEDPAYMENT.CAPTURE.REVERSEDBILLING.SUBSCRIPTION.CREATEDBILLING.SUBSCRIPTION.ACTIVATEDBILLING.SUBSCRIPTION.UPDATEDBILLING.SUBSCRIPTION.CANCELLEDBILLING.SUBSCRIPTION.SUSPENDEDBILLING.SUBSCRIPTION.EXPIREDBILLING.SUBSCRIPTION.PAYMENT.FAILEDVAULT.PAYMENT-TOKEN.CREATEDVAULT.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:
| Column | Value |
|---|---|
code | A short identifier for the gateway (e.g. paypal-live, paypal-stage). |
provider_type | paypal |
publishable_key | The PayPal app's client_id. The SDK sends this to the PayPal JS SDK as the client-id query parameter. |
secret_key | The PayPal app's client_secret. Used server-side to call PayPal REST endpoints. |
webhook_signing_secret | The PayPal webhook id captured in step 2. |
environment | stage 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:
- Buyer clicks the PayPal button (rendered by the PayPal JS SDK).
- FloPay creates a PayPal order with the line items and totals.
- PayPal opens the approval window (or in-app browser flow).
- On approval, FloPay captures the order against PayPal and processes the result against the FloPay session.
PAYMENT.CAPTURE.COMPLETEDarrives via webhook and maps toitem.purchasedon the outbound Flo webhook.
Subscriptions
Subscriptions use PayPal's Subscriptions API. PayPal — not Flo — drives the recurring billing cycle.
| Event | PayPal API | Flo outbound webhook |
|---|---|---|
| Signup | Create + approve a PayPal subscription | subscription.created, subscription.renewed (on the first cycle paid) |
| Recurring auto-charge | PayPal's native renewal cycle | subscription.renewed |
| Pause | POST /v1/billing/subscriptions/{id}/suspend | subscription.paused |
| Resume | POST /v1/billing/subscriptions/{id}/activate | subscription.resumed |
| Cancel | POST /v1/billing/subscriptions/{id}/cancel | subscription.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:
- On the upsell page, call the existing FloPay automatic-payment-button or saved-payment-method API — the same surface used for saved cards.
- FloPay locates the
user_payment_methodoftype='paypal'for the user, and uses the vaulted token to charge PayPal directly. VAULT.PAYMENT-TOKEN.CREATEDarrives at the first checkout;VAULT.PAYMENT-TOKEN.DELETEDarrives 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.purchasedarrives on the client webhook. - Subscription signup: create a subscription via direct PayPal; confirm
subscription.createdand (on the first renewal)subscription.renewed. - Recurring renewal: use the PayPal sandbox's accelerated billing cycle to trigger a renewal; confirm
subscription.renewedon the next cycle. - Pause / resume: pause the subscription; confirm
subscription.paused. Resume; confirmsubscription.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.CREATEDarrived 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.overduearrives. - 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.paypalisnullfor 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=paypalgateway record (see Setup). Sessions created after that point returngateways.paypalpopulated and the SDK switches automatically. - The breaking API response change (removal of
gatewayandgatewayData, addition ofgateways) 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=paypalgateway record for that client. New sessions dropgateways.paypaland the SDK falls back to the Stripe path immediately.
Related
- PayPal overview — path-selection summary
- PayPal via Stripe — fallback path
- PayPal webhook events — inbound catalog + outbound mapping
CheckoutGatewaysDto— session response shape