PayPal
Connect PayPal to FloPay and configure checkout and webhook handling.
Overview
FloPay supports two PayPal paths and selects one from the checkout session response:
| Path | When it renders | Best fit |
|---|---|---|
| Direct PayPal | gateways.paypal is present because the client has an active PayPal gateway. | New integrations and buyers using Facebook, Meta, or Instagram in-app browsers. |
| PayPal via Stripe | gateways.paypal is absent or null. | Existing Stripe-only integrations. |
The paths are mutually exclusive per session. PayPal never renders twice, and FloPayCheckout switches automatically without user-agent checks or client-specific props.
showPayPal is the SDK-level toggle for the entire PayPal gateway. It controls both the direct and Stripe-rendered paths and defaults to true.
<FloPayCheckout sessionId={sessionId} showPayPal={false} onComplete={handleSuccess} />Direct PayPal loads the PayPal JS SDK with the active gateway's client ID and environment. It avoids the popup and return-tab problems that affect Stripe-rendered PayPal inside Facebook, Meta, and Instagram in-app browsers.
What you can do
- Offer direct PayPal for one-time purchases and subscriptions in FloPay Checkout.
- Process direct PayPal refunds and reuse vaulted PayPal payment tokens for eligible follow-up purchases.
- Verify PayPal events and receive provider-neutral FloPay webhook events for payment and subscription changes.
Who this is for
This connector is for merchants that want PayPal to appear as a direct checkout option. FloPay client owners and admins can configure the gateway. Developers can integrate it through the SDK or current checkout-session response.
Prerequisites
- A PayPal developer account with access to create REST API apps and webhooks.
- A FloPay client where you are an owner or admin.
- An active, user-owned FloPay API token if you configure the gateway through the REST API.
- Separate PayPal apps for sandbox and live environments, with the required product and Disputes API access.
Setup
Create one gateway record per environment. A stage gateway uses a PayPal sandbox app. A production gateway uses a PayPal live app. Moving to production requires a separate record.
Client owners and admins can manage gateways through Settings > Gateways or the settings/gateways API. Client Basic authentication uses the client identifier as the username and an active user-owned API token as the password. Members cannot manage gateways, and all operations are scoped to the authenticated client.
The setup sequence uses POST /v1/settings/gateways, PATCH /v1/settings/gateways/:id, POST /v1/settings/gateways/:id/test, and POST /v1/settings/gateways/:id/activate.
Gateway lifecycle
| State | Meaning | Available actions |
|---|---|---|
draft | New or incomplete, and not used for payments. | Edit, test, activate |
active | Complete, validated, and eligible for checkout. | Edit settings, replace credentials, test, deactivate |
inactive | Complete but deactivated. | Edit, test, reactivate |
Every gateway begins as a draft. Activation requires complete credentials and a PayPal validation. Deactivation keeps the record but removes it from new sessions. Reactivation validates it again. PayPal app identity and environment are immutable after the first successful validation.
1. Create sandbox and live apps
In the PayPal developer dashboard, create a REST API app for each required environment. Enable Orders v2, Subscriptions, Vault, Webhooks, and Disputes API access in both sandbox and live.
Without Disputes API access in both sandbox and live, FloPay cannot classify v2 capture reversals from dispute evidence. Those reversals fall back to refunds and are surfaced for review.
2. Register the webhook
On the matching PayPal app, register the callback URL shown on the gateway's Webhook setup card:
https://<flopay-webhook-host>/webhooks/paypalOn the same app, subscribe to the 16 PayPal events listed under Webhook events. PayPal returns a webhook ID after registration.
The subscription must include the reversal signals used for financial classification:
PAYMENT.SALE.REVERSEDPAYMENT.CAPTURE.REVERSED
3. Save credentials
| FloPay field | PayPal value |
|---|---|
providerType | paypal |
publishableKey | REST app client ID |
secretKey | REST app client secret, write-only |
webhookSigningSecret | PayPal webhook ID, write-only |
environment | stage for sandbox or production for live |
webhookSigningSecret stores the PayPal webhook ID, not an HMAC secret. FloPay passes it to PayPal's hosted /v1/notifications/verify-webhook-signature endpoint. Read responses expose only configured-state metadata for write-only fields. Omitted credential fields keep their stored values during an update.
curl -X POST https://api.flopay.com/v1/settings/gateways \
-u "$FLOPAY_CLIENT_ID:$FLOPAY_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "PayPal live",
"providerType": "paypal",
"environment": "production",
"currency": "USD"
}'
curl -X PATCH https://api.flopay.com/v1/settings/gateways/GATEWAY_ID \
-u "$FLOPAY_CLIENT_ID:$FLOPAY_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"publishableKey": "PAYPAL_CLIENT_ID",
"secretKey": "PAYPAL_CLIENT_SECRET",
"webhookSigningSecret": "PAYPAL_WEBHOOK_ID"
}'
curl -X POST https://api.flopay.com/v1/settings/gateways/GATEWAY_ID/test \
-u "$FLOPAY_CLIENT_ID:$FLOPAY_API_TOKEN"
curl -X POST https://api.flopay.com/v1/settings/gateways/GATEWAY_ID/activate \
-u "$FLOPAY_CLIENT_ID:$FLOPAY_API_TOKEN"FloPay validates the app credentials and webhook ID before saving or activating. A failed update is atomic and leaves the previous configuration untouched. Once active, new checkout sessions include gateways.paypal.
Supported features
| Feature | Support |
|---|---|
| One-time payments | PayPal Orders v2 approval and capture |
| Recurring billing | PayPal subscriptions, renewals, pauses, resumes, and cancellations |
| Refunds | Refunds against direct PayPal capture IDs |
| Vaulted payment methods | Token storage and reuse after a direct PayPal purchase |
| Webhooks | Hosted signature verification and normalized FloPay events |
| Reversals | Refund or chargeback classification from PayPal evidence |
Direct PayPal
Direct PayPal renders when session.gateways.paypal is present. The SDK loads PayPal using that gateway's publishableKey and environment. When the entry is missing, the SDK uses the Stripe-rendered fallback instead.
API shape
The session response has this shape:
{
"id": "sess_abc123",
"amount": 4999,
"currency": "usd",
"gateways": {
"stripe": {
"publishableKey": "pk_live_xxx",
"environment": "production"
},
"paypal": {
"publishableKey": "PAYPAL_CLIENT_ID",
"environment": "production"
}
}
}The old top-level gateway and gatewayData.paypalPublishableKey fields have been replaced by the gateways map. The FloPay SDK consumes the current shape automatically. Direct API consumers must update their session reader.
SDK integration
FloPayCheckout and SplitCardForm consume session.gateways and select the path automatically:
import { FloPayCheckout } from '@flopay/react';
export function Checkout({ sessionId }: { sessionId: string }) {
return (
<FloPayCheckout
sessionId={sessionId}
onComplete={(result) => {
if (result.status === 'succeeded') window.location.href = '/success';
}}
onError={(error) => console.error(error.message)}
/>
);
}Each PayPal gateway has its own loader, so sandbox and live credentials do not leak between environments.
Lifecycle
One-time purchases
Direct one-time purchases use the PayPal Orders v2 API:
- The buyer selects PayPal.
- FloPay creates an order with line items and totals.
- PayPal opens the approval flow.
- FloPay captures the approved order against the checkout session.
PAYMENT.CAPTURE.COMPLETEDmaps toitem.purchasedandinvoice.paid.
Subscriptions
Subscriptions use PayPal's Subscriptions API. PayPal drives the billing cycle.
| Action | PayPal operation | Flo outbound event |
|---|---|---|
| Signup | Create and approve a subscription | subscription.created, then subscription.renewed when paid |
| Renewal | Native PayPal 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 |
FloPay does not issue off-session retries for PayPal subscriptions. BILLING.SUBSCRIPTION.PAYMENT.FAILED arrives after PayPal has exhausted its retry window.
Refunds
Refund a direct capture against its PayPal capture ID. FloPay calls POST /v2/payments/captures/{capture_id}/refund; the resulting PAYMENT.CAPTURE.REFUNDED maps to item.refunded.
Vaulted token reuse
After a first direct PayPal purchase, FloPay can store the vaulted token on user_payment_method with type='paypal'. An upsell can use the existing automatic-payment-button or saved-payment-method surface without rendering the PayPal button again. VAULT.PAYMENT-TOKEN.CREATED stores the method, and VAULT.PAYMENT-TOKEN.DELETED removes it.
Webhook events
Direct PayPal events are normalized into the same outbound subscription.*, item.*, and invoice.* families as other gateways.
FloPay subscribes to the following 16 PayPal webhook events.
| PayPal event | Purpose |
|---|---|
CHECKOUT.ORDER.APPROVED | The buyer approved a one-time order. |
PAYMENT.CAPTURE.COMPLETED | A capture succeeded. |
PAYMENT.CAPTURE.DENIED | PayPal denied a capture. |
PAYMENT.CAPTURE.PENDING | A capture is under review. |
PAYMENT.CAPTURE.REFUNDED | A capture was refunded. |
PAYMENT.CAPTURE.REVERSED | A capture was reversed and requires classification. |
PAYMENT.SALE.REVERSED | A subscription Sale was reversed and carries a reason code. |
BILLING.SUBSCRIPTION.CREATED | A subscription was created. |
BILLING.SUBSCRIPTION.ACTIVATED | A subscription became active. |
BILLING.SUBSCRIPTION.UPDATED | Subscription details changed. |
BILLING.SUBSCRIPTION.CANCELLED | A subscription was cancelled. |
BILLING.SUBSCRIPTION.SUSPENDED | A subscription was paused. |
BILLING.SUBSCRIPTION.EXPIRED | A subscription reached its end. |
BILLING.SUBSCRIPTION.PAYMENT.FAILED | PayPal exhausted retries for a recurring charge. |
VAULT.PAYMENT-TOKEN.CREATED | A vaulted PayPal token was created. |
VAULT.PAYMENT-TOKEN.DELETED | A vaulted PayPal token was removed. |
Signature verification
FloPay sends the webhook ID and delivery headers to PayPal's /v1/notifications/verify-webhook-signature endpoint before processing an event. Local certificate verification is not used in this phase.
Inbound to outbound mapping
| PayPal event | Outbound Flo event | Notes |
|---|---|---|
CHECKOUT.ORDER.APPROVED | (no outbound) | Internal trigger to capture the order. |
PAYMENT.CAPTURE.COMPLETED | item.purchased, invoice.paid | Includes a successful first subscription cycle. |
PAYMENT.CAPTURE.DENIED | invoice.overdue | The invoice remains collectible. |
PAYMENT.CAPTURE.PENDING | (no outbound) | FloPay waits for a final capture state. |
PAYMENT.CAPTURE.REFUNDED | item.refunded | One-to-one with the capture refund. |
PAYMENT.CAPTURE.REVERSED | invoice.updated, plus either item.charged_back or item.refunded | FloPay queries the Disputes API before selecting one path. |
PAYMENT.SALE.REVERSED | invoice.updated, plus either item.charged_back or item.refunded, except for chargeback_reimbursement | A chargeback_reimbursement restores the invoice without any item event. |
BILLING.SUBSCRIPTION.CREATED | subscription.created | A subscription was created. |
BILLING.SUBSCRIPTION.ACTIVATED | subscription.created or subscription.reactivated | Depends on prior FloPay state. |
BILLING.SUBSCRIPTION.UPDATED | subscription.updated | Details changed. |
BILLING.SUBSCRIPTION.CANCELLED | subscription.cancelled | The subscription was cancelled. |
BILLING.SUBSCRIPTION.SUSPENDED | subscription.paused | The subscription was paused. |
BILLING.SUBSCRIPTION.EXPIRED | subscription.expired | The subscription ended. |
BILLING.SUBSCRIPTION.PAYMENT.FAILED | invoice.overdue | PayPal exhausted its retry window, so FloPay does not retry. |
VAULT.PAYMENT-TOKEN.CREATED | (no outbound) | FloPay stores the token for upsell reuse. |
VAULT.PAYMENT-TOKEN.DELETED | (no outbound) | FloPay removes the stored token. |
Reversal classification
PAYMENT.SALE.REVERSED
PayPal v1 Sale reversal reasons are trimmed, compared case-insensitively, and normalized so hyphens and underscores are equivalent.
| Classification | Normalized reason_code values |
|---|---|
| Chargeback | chargeback, chargeback_reimbursement, chargeback_settlement, unauthorized_spoof, buyer_complaint, unauthorized_claim, guarantee |
| Refund | refund, admin_reversal, admin_fraud_reversal, adjustment_reversal |
other, an unrecognised value, or an absent reason falls back to a refund and is surfaced for review. The first classification is kept for the provider reversal identity, so a replay cannot become both a refund and a chargeback.
chargeback_reimbursement restores the invoice through invoice.updated only, with no second item.charged_back and no item.refunded.
PAYMENT.CAPTURE.REVERSED
PayPal v2 PAYMENT.CAPTURE.REVERSED carries no reason code. FloPay queries the PayPal Disputes API by original capture ID. A recognized dispute at CHARGEBACK, PRE_ARBITRATION, or ARBITRATION is positive chargeback evidence.
A missing capture ID or client, empty or malformed response, failed API request, or otherwise unclassifiable evidence uses the fallback refund path and is surfaced for review. FloPay then freezes that classification for the provider reversal identity.
Sandbox testing
Provision a stage gateway for the PayPal sandbox, then verify:
- One-time purchase: complete payment and receive
item.purchased. - Subscription signup: receive
subscription.createdand a paid-cycle renewal. - Recurring renewal: use accelerated sandbox billing and receive
subscription.renewed. - Pause, resume, and cancel: confirm each subscription transition.
- Refund: refund a capture and receive
item.refunded. - Vaulted upsell: reuse a saved token without showing the button again.
- Meta in-app browser: complete checkout from a Facebook or Instagram mobile surface.
- Failed renewal: receive
invoice.overdueafter PayPal finishes retrying. - Webhook timeline: confirm every inbound event was processed without pending retries.
Migration
- Existing Stripe-rendered PayPal clients continue unchanged while
gateways.paypalis absent. - Opt in to direct PayPal by creating and activating a PayPal gateway. New sessions then include
gateways.paypal. - Update direct session readers from the retired
gatewayandgatewayDatafields to thegatewaysmap. SDK users do not need to change their reader. - Rolling back means deactivating the PayPal gateway. New sessions omit
gateways.paypaland return to the Stripe path.
Limitations
- Direct PayPal requires separate sandbox and live apps and gateway records.
- FloPay relies on PayPal's hosted webhook verification endpoint in this phase.
- Unclassifiable capture reversals fall back to refund classification and require review.
- PayPal owns recurring retry timing. FloPay does not add off-session retries.
- Stripe-rendered PayPal remains unreliable in Facebook, Meta, and Instagram in-app browsers.