Webhooks
Inbound PayPal events FloPay subscribes to and how they map to outbound Flo webhooks.
PayPal Events
When a client provisions a direct PayPal gateway, FloPay subscribes to a fixed set of PayPal webhook events at the gateway level and normalizes them into the existing outbound Flo webhook families.
Direct PayPal events arrive at your client webhook endpoint as the provider-neutral subscription.*, item.*, and invoice.* events documented in Events, including item.charged_back. This page is reference material for what happens upstream — your handler code does not need to branch on gateway. The outbound payload shape is identical regardless of which gateway produced the underlying event.
Subscribed PayPal Events
FloPay subscribes to the following 16 PayPal webhook events. These are configured on the PayPal app you provision during direct PayPal setup.
| PayPal event | Purpose |
|---|---|
CHECKOUT.ORDER.APPROVED | Buyer approved a one-time order in the PayPal flow. |
PAYMENT.CAPTURE.COMPLETED | One-time capture succeeded. |
PAYMENT.CAPTURE.DENIED | One-time capture was denied by PayPal. |
PAYMENT.CAPTURE.PENDING | Capture is pending PayPal review. |
PAYMENT.CAPTURE.REFUNDED | A previous capture was refunded. |
PAYMENT.CAPTURE.REVERSED | A previous capture was reversed (chargeback / dispute). |
PAYMENT.SALE.REVERSED | A subscription Sale was reversed and needs reason-code classification. |
BILLING.SUBSCRIPTION.CREATED | Subscription created in PayPal. |
BILLING.SUBSCRIPTION.ACTIVATED | Subscription transitioned to active. |
BILLING.SUBSCRIPTION.UPDATED | Subscription details changed (plan, quantity, etc.). |
BILLING.SUBSCRIPTION.CANCELLED | Subscription was cancelled. |
BILLING.SUBSCRIPTION.SUSPENDED | Subscription was suspended (paused). |
BILLING.SUBSCRIPTION.EXPIRED | Subscription reached its end and was not renewed. |
BILLING.SUBSCRIPTION.PAYMENT.FAILED | A recurring charge failed after PayPal exhausted retries. |
VAULT.PAYMENT-TOKEN.CREATED | A vaulted PayPal payment token was created — enables saved-payment upsells. |
VAULT.PAYMENT-TOKEN.DELETED | A vaulted PayPal payment token was removed. |
Signature Verification
FloPay verifies each delivery by calling PayPal's hosted /v1/notifications/verify-webhook-signature endpoint with the webhook id stored in the gateway record's webhook_signing_secret column. Local certificate-based verification is deferred — every inbound PayPal event hits PayPal's verification endpoint before it is processed.
Inbound → 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 | Emitted on successful one-time capture, including the first cycle of a subscription. |
PAYMENT.CAPTURE.DENIED | invoice.overdue | The invoice stays collectible; PayPal denied this specific capture. |
PAYMENT.CAPTURE.PENDING | (no outbound) | FloPay holds processing until the capture resolves to completed, denied, or reversed. |
PAYMENT.CAPTURE.REFUNDED | item.refunded | One-to-one with refunding the capture id. |
PAYMENT.CAPTURE.REVERSED | invoice.updated, plus either item.charged_back or item.refunded | The v2 event has no reason code, so FloPay enriches it through PayPal's Disputes API before selecting exactly one item-event path. |
PAYMENT.SALE.REVERSED | invoice.updated, plus either item.charged_back or item.refunded, except for chargeback_reimbursement | FloPay uses the normalized v1 reason_code. A chargeback_reimbursement restores the invoice without any item event. |
BILLING.SUBSCRIPTION.CREATED | subscription.created | |
BILLING.SUBSCRIPTION.ACTIVATED | subscription.created or subscription.reactivated | subscription.created on first activation; subscription.reactivated when a previously cancelled subscription is restored. |
BILLING.SUBSCRIPTION.UPDATED | subscription.updated | |
BILLING.SUBSCRIPTION.CANCELLED | subscription.cancelled | |
BILLING.SUBSCRIPTION.SUSPENDED | subscription.paused | |
BILLING.SUBSCRIPTION.EXPIRED | subscription.expired | |
BILLING.SUBSCRIPTION.PAYMENT.FAILED | invoice.overdue | PayPal exhausted its retry window — Flo does not issue further off-session retries. |
VAULT.PAYMENT-TOKEN.CREATED | (no outbound) | Internal: FloPay stores the token on user_payment_method with type='paypal' for upsell reuse. |
VAULT.PAYMENT-TOKEN.DELETED | (no outbound) | Internal: 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, any unrecognised value, or an absent reason falls back to a refund and is surfaced for review. The first classification is kept for that provider reversal identity so a replay cannot produce both a refund and a chargeback.
chargeback_reimbursement is a reinstatement: it 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 PayPal's Disputes API using the original capture id. A matching dispute with a recognized reason and an escalated CHARGEBACK, PRE_ARBITRATION, or ARBITRATION lifecycle stage is treated as positive chargeback evidence.
A missing capture id or PayPal client, an empty or malformed response, a failed API request, or otherwise unclassifiable evidence uses the fallback refund path and is surfaced for review. That classification is then frozen for the provider reversal identity.
subscription.resumed is emitted when a previously suspended subscription returns to active. Depending on the originating PayPal flow this can arrive via BILLING.SUBSCRIPTION.ACTIVATED or BILLING.SUBSCRIPTION.UPDATED — FloPay normalizes on its own state transition, not the inbound PayPal event name.
Recurring Billing Behaviour
PayPal — not Flo — drives the recurring billing cycle for direct PayPal subscriptions. Renewals fire PAYMENT.CAPTURE.COMPLETED against the subscription, and FloPay emits subscription.renewed (with the linked invoice.paid).
Off-session retries are PayPal's responsibility. FloPay does not retry a failed PayPal recurring charge — once BILLING.SUBSCRIPTION.PAYMENT.FAILED arrives, PayPal has already exhausted its dunning window.
Related
- Events — full outbound event catalog and examples
- Payloads — outbound field-level schema
- Overview — outbound webhook contract
- Direct PayPal — gateway setup and lifecycle
Direct PayPal
Provision a direct PayPal gateway and render PayPal through the PayPal JS SDK. Works inside Facebook, Meta, and Instagram in-app browsers.
Payments and authorisations
Payment reporting fields, manual-capture states, capture and cancellation operations, authentication, errors, and retry rules.