Documentation
Connectors
Available

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:

PathWhen it rendersBest fit
Direct PayPalgateways.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 Stripegateways.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.
  • Follow PayPal disputes through the provider-neutral dispute.* lifecycle events.

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

StateMeaningAvailable actions
draftNew or incomplete, and not used for payments.Edit, test, activate
activeComplete, validated, and eligible for checkout.Edit settings, replace credentials, test, deactivate
inactiveComplete 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/paypal

On the same app, subscribe to the 24 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.REVERSED
  • PAYMENT.CAPTURE.REVERSED

It must also include the dispute notifications that drive the outbound dispute.* lifecycle, which FloPay cannot produce without them:

  • CUSTOMER.DISPUTE.CREATED
  • CUSTOMER.DISPUTE.UPDATED
  • CUSTOMER.DISPUTE.RESOLVED

3. Save credentials

FloPay fieldPayPal value
providerTypepaypal
publishableKeyREST app client ID
secretKeyREST app client secret, write-only
webhookSigningSecretPayPal webhook ID, write-only
environmentstage 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. sessionMode is optional and defaults to both; single-gateway merchants need not set it. See Gateway settings.

Supported features

FeatureSupport
One-time paymentsPayPal Orders v2 approval and capture
Recurring billingPayPal subscriptions, renewals, pauses, resumes, and cancellations
RefundsRefunds against direct PayPal capture IDs
Vaulted payment methodsToken storage and reuse after a direct PayPal purchase
WebhooksHosted signature verification and normalized FloPay events
ReversalsRefund or chargeback classification from PayPal evidence
DisputesPayPal dispute notifications normalized to the provider-neutral dispute.* lifecycle

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:

  1. The buyer selects PayPal.
  2. FloPay creates an order with line items and totals.
  3. PayPal opens the approval flow.
  4. FloPay captures the approved order against the checkout session.
  5. PAYMENT.CAPTURE.COMPLETED maps to item.purchased and invoice.paid.

Subscriptions

Subscriptions use PayPal's Subscriptions API. PayPal drives the billing cycle.

ActionPayPal operationFlo outbound event
SignupCreate and approve a subscriptionsubscription.created, then subscription.renewed when paid
RenewalNative PayPal cyclesubscription.renewed
PausePOST /v1/billing/subscriptions/{id}/suspendsubscription.paused
ResumePOST /v1/billing/subscriptions/{id}/activatesubscription.resumed
CancelPOST /v1/billing/subscriptions/{id}/cancelsubscription.cancelled
ExpirePayPal lifecycle endsubscription.expired
Failed paymentPayPal recurring failureinvoice.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.*, invoice.*, and dispute.* families as other gateways. See Dispute events for the lifecycle contract.

FloPay subscribes to the following 24 PayPal webhook events.

PayPal eventPurpose
CHECKOUT.ORDER.APPROVEDThe buyer approved a one-time order.
PAYMENT.CAPTURE.COMPLETEDA capture succeeded.
PAYMENT.CAPTURE.DENIEDPayPal denied a capture.
PAYMENT.CAPTURE.PENDINGA capture is under review.
PAYMENT.CAPTURE.REFUNDEDA capture was refunded.
PAYMENT.CAPTURE.REVERSEDA capture was reversed and requires classification.
PAYMENT.REFUND.PENDINGA refund is pending at PayPal.
PAYMENT.REFUND.FAILEDA refund failed and the funds stay with the merchant.
PAYMENT.SALE.COMPLETEDA recurring subscription charge settled as a v1 Sale.
PAYMENT.SALE.REFUNDEDA v1 Sale was refunded, fully or partially.
PAYMENT.SALE.REVERSEDA subscription Sale was reversed and carries a reason code.
CUSTOMER.DISPUTE.CREATEDA buyer opened a dispute. Required for the dispute lifecycle.
CUSTOMER.DISPUTE.UPDATEDA dispute changed, for example its stage, state, or seller response deadline.
CUSTOMER.DISPUTE.RESOLVEDA dispute reached an outcome.
BILLING.SUBSCRIPTION.CREATEDA subscription was created.
BILLING.SUBSCRIPTION.ACTIVATEDA subscription became active.
BILLING.SUBSCRIPTION.UPDATEDSubscription details changed.
BILLING.SUBSCRIPTION.CANCELLEDA subscription was cancelled.
BILLING.SUBSCRIPTION.SUSPENDEDA subscription was paused.
BILLING.SUBSCRIPTION.EXPIREDA subscription reached its end.
BILLING.SUBSCRIPTION.PAYMENT.FAILEDPayPal exhausted retries for a recurring charge.
VAULT.PAYMENT-TOKEN.CREATEDA vaulted PayPal token was created.
VAULT.PAYMENT-TOKEN.DELETION-INITIATEDPayPal began removing a vaulted token.
VAULT.PAYMENT-TOKEN.DELETEDA 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 eventOutbound Flo eventNotes
CHECKOUT.ORDER.APPROVED(no outbound)Internal trigger to capture the order.
PAYMENT.CAPTURE.COMPLETEDitem.purchased, invoice.paidIncludes a successful first subscription cycle.
PAYMENT.CAPTURE.DENIEDinvoice.overdueThe invoice remains collectible.
PAYMENT.CAPTURE.PENDING(no outbound)FloPay waits for a final capture state.
PAYMENT.CAPTURE.REFUNDEDitem.refundedOne-to-one with the capture refund.
PAYMENT.CAPTURE.REVERSEDinvoice.updated, plus either item.charged_back or item.refundedFloPay queries the Disputes API before selecting one path.
PAYMENT.REFUND.PENDING(no outbound)FloPay tracks the pending refund state internally.
PAYMENT.REFUND.FAILED(no outbound)FloPay reconciles the failed refund internally.
PAYMENT.SALE.COMPLETEDinvoice.created and invoice.paid, plus subscription.renewed on a renewal cycle and item.purchased for one-time items bundled into the opening cycleRecurring PayPal charges settle as v1 Sales, so subscription cycles arrive here rather than as captures. The opening charge adopts the synchronous first-cycle record and is not counted twice.
PAYMENT.SALE.REFUNDEDinvoice.updated, item.refundedFloPay fetches the parent Sale, records the refund, and marks the purchase refunded only when the refund is full.
PAYMENT.SALE.REVERSEDinvoice.updated, plus either item.charged_back or item.refunded, except for chargeback_reimbursementA chargeback_reimbursement restores the invoice without any item event.
BILLING.SUBSCRIPTION.CREATEDsubscription.createdA subscription was created.
BILLING.SUBSCRIPTION.ACTIVATEDsubscription.created or subscription.reactivatedDepends on prior FloPay state.
BILLING.SUBSCRIPTION.UPDATEDsubscription.updatedDetails changed.
BILLING.SUBSCRIPTION.CANCELLEDsubscription.cancelledThe subscription was cancelled.
BILLING.SUBSCRIPTION.SUSPENDEDsubscription.pausedThe subscription was paused.
BILLING.SUBSCRIPTION.EXPIREDsubscription.expiredThe subscription ended.
BILLING.SUBSCRIPTION.PAYMENT.FAILEDinvoice.overduePayPal 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.DELETION-INITIATED(no outbound)FloPay reconciles the deletion against its own payment-method record before the deleted notification arrives.
VAULT.PAYMENT-TOKEN.DELETED(no outbound)FloPay removes the stored token.
CUSTOMER.DISPUTE.CREATEDdispute.createdFloPay hydrates the dispute from the Disputes API and starts the provider-neutral lifecycle. A dispute first seen past the open state is followed by the matching dispute.updated, dispute.won, or dispute.lost with the next sequence. Money movement still arrives through the reversal rows above.
CUSTOMER.DISPUTE.UPDATEDdispute.updated or (no new outbound)A material change emits dispute.updated. An appealable outcome arrives here with phase: "appeal" and is not terminal. Repeated or stale notifications emit nothing new.
CUSTOMER.DISPUTE.RESOLVEDdispute.won, dispute.lost, or (no outbound)A resolution in the seller's favor, or a dispute cancelled by the buyer, emits dispute.won; a resolution in the buyer's favor emits dispute.lost. An outcome FloPay cannot classify is retained for review and emits no event.

Dispute lifecycle

The CUSTOMER.DISPUTE.* notifications feed only the provider-neutral dispute lifecycle. They never move money on their own: withdrawals and reinstatements keep arriving through PAYMENT.SALE.REVERSED and PAYMENT.CAPTURE.REVERSED as item.charged_back and invoice.updated.

FloPay reads the dispute's lifecycle stage and state from the Disputes API. INQUIRY and CHARGEBACK stages map to phase: "initial"; PRE_ARBITRATION, ARBITRATION, and an appealable resolution map to phase: "appeal". status is open while PayPal or the buyer holds the next step, with actionRequired set to true and responseDueAt populated when a seller response is required; it is under_review while PayPal is deciding. PayPal reasons are normalized to the values in Dispute Payload, and the outbound payload never carries the PayPal dispute id. States and outcomes FloPay cannot classify are retained for review rather than reported.

Inquiry-stage activity does not reach the same provider-neutral family on every gateway. A PayPal INQUIRY is a Disputes API case, so it arrives as dispute.* with phase: "initial"; the equivalent Stripe warning statuses are not formal disputes and arrive as pre_dispute_alert.*, described under Stripe dispute and Pre-Dispute Alert lifecycle. Subscribe to both families when you run both gateways.

Reversal classification

PAYMENT.SALE.REVERSED

PayPal v1 Sale reversal reasons are trimmed, compared case-insensitively, and normalized so hyphens and underscores are equivalent.

ClassificationNormalized reason_code values
Chargebackchargeback, chargeback_reimbursement, chargeback_settlement, unauthorized_spoof, buyer_complaint, unauthorized_claim, guarantee
Refundrefund, 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.created and 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.overdue after PayPal finishes retrying.
  • Webhook timeline: confirm every inbound event was processed without pending retries.
  • Dispute lifecycle: open a sandbox dispute and receive dispute.created, then resolve it and receive dispute.won or dispute.lost.

Migration

  • Existing Stripe-rendered PayPal clients continue unchanged while gateways.paypal is 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 gateway and gatewayData fields to the gateways map. SDK users do not need to change their reader.
  • Rolling back means deactivating the PayPal gateway. New sessions omit gateways.paypal and 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.

On this page