Stripe
Connect Stripe to FloPay and configure payment methods and webhooks.
What you can do
- Accept cards and eligible wallets for one-time purchases, and use Stripe subscriptions for recurring billing.
- Control which eligible Stripe payment methods appear in FloPay Checkout.
- Verify Stripe events and receive provider-neutral FloPay webhook events for payment, subscription, dispute, and pre-dispute alert changes.
Who this is for
This connector is for merchants that process payments with a Stripe account and want to present those payment methods through FloPay Checkout. FloPay client owners and admins can configure the gateway. Developers can use the same connection through the SDK and REST API.
Prerequisites
- A Stripe account with access to its API keys and webhook settings.
- 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.
- Access to each checkout hostname if you plan to enable Apple Pay or Google Pay.
Setup
FloPay stores your Stripe connection as a gateway record. One gateway represents one provider account in one environment. Use a stage gateway for Stripe test mode and a production gateway for Stripe live mode. Moving from test to live means creating a separate gateway record, not changing an existing record in place.
Client owners and admins can manage gateways in Settings > Gateways or through the settings/gateways REST API. Members cannot manage gateway settings. Client Basic authentication uses the client identifier as the username and an active user-owned API token as the password. Access is always 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. It is not used for payments. | Edit, test, activate |
active | Complete, validated, and eligible for payment processing. | Edit settings, replace credentials, test, deactivate |
inactive | Complete but deactivated. | Edit, test, reactivate |
Every new gateway starts as a draft. Activation requires all three credential fields and a live provider check. Deactivation stops the gateway from handling new payments but keeps its configuration. Reactivation validates the connection again. Gateways are never deleted.
FloPay captures the canonical Stripe account ID (acct_...) during the first successful connection validation. The provider, environment, and connected account are immutable after validation. Connecting the same Stripe account twice in the same environment returns 409.
Credentials and secret handling
Open Stripe Dashboard > Developers > API keys and copy:
publishableKey:pk_test_...for stage orpk_live_...for production.secretKey:sk_test_...,sk_live_..., or a restrictedrk_...key with account read access.webhookSigningSecret: the endpoint signing secret beginning withwhsec_.
The environment must match the key mode. FloPay rejects test keys on a production gateway and live keys on a stage gateway.
publishableKey is returned in read responses. secretKey and webhookSigningSecret are write-only. Read responses expose only secretKeyConfigured, webhookSigningSecretConfigured, and timestamps. There is no way to retrieve a submitted secret through the API, Dashboard, or support.
To rotate a credential, submit its replacement in a normal update. Omitted credential fields keep their stored values. FloPay validates the complete resulting credential set before it saves anything, so a failed validation leaves the previous configuration untouched.
Never put real keys or signing secrets in code samples, screenshots, URLs, logs, or support tickets.
1. Create a draft
In the Dashboard, open Settings > Gateways > Add gateway, choose Stripe and the environment, then create the draft. Credentials are optional until you are ready to validate the connection. sessionMode is optional and defaults to both; single-gateway merchants need not set it. See Gateway settings.
curl -X POST https://api.flopay.com/v1/settings/gateways \
-u "$FLOPAY_CLIENT_ID:$FLOPAY_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "UK Ltd Stripe (live)",
"providerType": "stripe",
"environment": "production",
"currency": "GBP",
"countries": ["GB", "IE"],
"priority": 0
}'2. Register the FloPay webhook
FloPay does not create the Stripe webhook automatically. In the matching test or live mode, open Developers > Webhooks, add a destination, and use the callback URL shown on the gateway's Webhook setup card:
https://<flopay-webhook-host>/webhooks/stripeThe URL is per provider, not per gateway. FloPay matches deliveries to the gateway using the account identity in the event. The host differs between FloPay stage and production, so copy the Dashboard value rather than constructing it. Subscribe to every event in Subscribed Stripe Events, then save the endpoint's whsec_... value as webhookSigningSecret.
Include charge.dispute.updated, radar.early_fraud_warning.created, and radar.early_fraud_warning.updated. Without them, FloPay cannot produce the outbound dispute.* and pre_dispute_alert.* events for this gateway.
3. Save and test credentials
Save publishableKey, secretKey, and webhookSigningSecret on the draft. A credential-bearing PATCH validates the resulting set and captures the Stripe account ID.
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": "pk_live_PLACEHOLDER",
"secretKey": "sk_live_PLACEHOLDER",
"webhookSigningSecret": "whsec_PLACEHOLDER"
}'
curl -X POST https://api.flopay.com/v1/settings/gateways/GATEWAY_ID/test \
-u "$FLOPAY_CLIENT_ID:$FLOPAY_API_TOKEN"The connection test probes Stripe without changing the gateway. Failures return a sanitized error and never echo key material.
4. Activate
Activation checks completeness, validates the stored credentials again, and moves the gateway to active.
curl -X POST https://api.flopay.com/v1/settings/gateways/GATEWAY_ID/activate \
-u "$FLOPAY_CLIENT_ID:$FLOPAY_API_TOKEN"Use the matching /deactivate operation to stop an active gateway. Calling /activate on an inactive gateway revalidates and reactivates it.
Supported features
| Feature | Support |
|---|---|
| One-time payments | Cards and eligible Stripe-rendered payment methods |
| Recurring billing | Stripe-managed subscriptions and renewal events |
| Wallets | Apple Pay and Google Pay on eligible devices and registered domains |
| PayPal | Stripe-rendered fallback when the direct PayPal connector is absent |
| Webhooks | Verified Stripe events normalized to FloPay event families |
| Disputes and alerts | Stripe disputes, inquiries, and Radar early fraud warnings normalized to the provider-neutral dispute.* and pre_dispute_alert.* lifecycles |
Payment methods
The Stripe gateway is dynamic. FloPay asks Stripe to render the methods enabled on the connected account and eligible for the current buyer. Enabling a supported method in Stripe does not require an SDK upgrade or frontend change.
showStripe controls the entire Stripe gateway. Its default value is true. Setting it to false hides cards, wallets, and all other Stripe-rendered methods. It does not select individual methods.
<FloPayCheckout sessionId={sessionId} onComplete={handleSuccess} />
<FloPayCheckout
sessionId={sessionId}
showStripe={false}
onComplete={handleSuccess}
/>The methods shown to a buyer depend on all of these conditions:
- The method is enabled for the connected account in Stripe.
- The buyer's locale, currency, and amount meet Stripe's rules.
- The browser and device support the method.
- Any required checkout domain is registered.
Stripe can provide cards, Apple Pay, Google Pay, Link, buy-now-pay-later methods, bank redirects, bank debits, and cash or voucher methods. Stripe's payment-method catalog is the current source of truth.
Narrowing the rendered methods
The session provides its eligible list as gateways.stripe.enabledPaymentMethods. Pass enabledPaymentMethods to render a subset. It narrows, never widens: naming a method the session does not advertise cannot enable it.
<FloPayCheckout
sessionId={sessionId}
enabledPaymentMethods={['apple_pay']}
onComplete={handleSuccess}
/>
<FloPayCheckout
sessionId={sessionId}
enabledPaymentMethods={[]}
showPayPal={false}
onComplete={handleSuccess}
/>An empty array means no optional methods and produces a card-only checkout. It does not fall back to the session list. Disable a single method account-wide in Stripe rather than setting showStripe={false}. Use the debug prop during local development to see which methods resolved for the current session.
Apple Pay
Apple Pay requires all of the following:
- Apple Pay enabled for the connected Stripe account.
- A Stripe-provided domain association file served from
/.well-known/apple-developer-merchantid-domain-associationon every checkout hostname. - Each hostname registered in the matching Stripe account and mode.
- HTTPS and a supported Apple device with Apple Pay configured.
Place the downloaded association file at public/.well-known/apple-developer-merchantid-domain-association. Next.js serves it from the root. Register production, staging, regional, and subdomains separately. Repeat the registration for every Stripe account that can serve the checkout.
Apple Pay appears in Safari on supported iPhone, iPad, and Mac devices. It does not appear on Android or Windows, and Chrome on Mac is not a supported Apple Pay surface. Stripe test mode still requires a real card in Wallet, although the card is not charged.
If the button does not appear, verify the file returns successfully over HTTPS, confirm the exact hostname is registered in the current Stripe mode, check the device and Wallet setup, and inspect the browser console for Stripe errors.
Google Pay
Google Pay uses Stripe's ExpressCheckoutElement. Register every checkout hostname under Stripe Dashboard > Settings > Payments > Payment method domains in the matching test and live environments. Unlike Apple Pay, Google Pay does not use the Apple domain association file.
Test on HTTPS with Chrome or another supported Chromium browser and a Google Pay-enabled profile or device. Register the hostname in every Stripe account whose publishable key may initialize FloPay. If the button works in one environment but not another, confirm the exact hostname and connected Stripe account in that environment.
PayPal via Stripe
FloPay supports a Stripe-rendered PayPal fallback alongside the direct PayPal connector. The session selects exactly one path:
- When
gateways.paypalis present, direct PayPal renders. - When
gateways.paypalis absent ornull, PayPal renders through Stripe'sExpressCheckoutElementusinggateways.stripe.publishableKey.
The paths are mutually exclusive per session, so PayPal never renders twice. FloPayCheckout handles selection, redirect state, and resume behavior automatically.
Stripe requires the PayPal ExpressCheckoutElement to use an Elements instance without paymentMethodCreation: 'manual', while card tokenization requires that setting. SplitCardForm therefore mounts separate card and PayPal Elements groups. The PayPal group receives amount and currency from the session.
The Stripe-rendered path does not work reliably inside Facebook, Meta, and Instagram in-app browsers. Merchants serving those buyers should configure the direct PayPal connector. Existing Stripe-only integrations keep using the fallback until a PayPal gateway becomes active.
Webhook events
FloPay verifies Stripe deliveries and normalizes them into the provider-neutral outbound events documented under Webhooks, including the dispute.* and pre_dispute_alert.* lifecycles described in Dispute events and Pre-Dispute Alert events. Merchant handlers do not need to branch on the gateway.
Subscribed Stripe Events
FloPay subscribes to the following 39 Stripe webhook events.
| Stripe event | Purpose |
|---|---|
charge.refunded | A charge was refunded, fully or partially. |
charge.dispute.created | A dispute opened; it records no amount when the notification contains no balance movement. |
charge.dispute.funds_withdrawn | Disputed funds left the merchant balance. |
charge.dispute.funds_reinstated | Disputed funds returned after a win. |
charge.dispute.closed | The dispute reached its final won or lost outcome. |
charge.dispute.updated | The dispute changed, for example its status, evidence deadline, or inquiry state. Required for the dispute and Pre-Dispute Alert lifecycles. |
customer.created | A Stripe customer was created. |
customer.updated | A Stripe customer's details changed. |
customer.deleted | A Stripe customer was deleted. |
customer.subscription.created | A Stripe subscription was created. |
customer.subscription.updated | Subscription details changed. |
customer.subscription.deleted | A Stripe subscription ended. |
customer.subscription.paused | A subscription was paused. |
customer.subscription.resumed | A subscription resumed. |
invoice.created | A draft invoice was created. |
invoice.updated | Invoice fields changed. |
invoice.deleted | A draft invoice was deleted. |
invoice.finalized | An invoice became collectible. |
invoice.sent | An invoice was sent to the customer. |
invoice.upcoming | Stripe rendered an upcoming invoice. |
invoice.will_be_due | An invoice is about to be due. |
invoice.payment_action_required | Invoice payment requires SCA or 3DS action. |
invoice.payment_failed | An invoice payment attempt failed. |
invoice.payment_succeeded | An invoice payment attempt succeeded; invoice.paid remains authoritative. |
invoice.paid | An invoice was paid in full. |
invoice.overdue | An invoice passed its due date. |
invoice.voided | An invoice was voided. |
invoice.marked_uncollectible | An invoice was marked uncollectible. |
payment_intent.succeeded | A one-off PaymentIntent succeeded. |
payment_intent.payment_failed | A PaymentIntent attempt failed. |
payment_intent.amount_capturable_updated | A manual-capture PaymentIntent was authorized and is awaiting capture. |
payment_intent.canceled | An uncaptured PaymentIntent was canceled or its hold expired. |
radar.early_fraud_warning.created | Stripe Radar raised an early fraud warning for a charge. Required for Pre-Dispute Alert events. |
radar.early_fraud_warning.updated | An early fraud warning changed, for example after the charge was refunded or disputed. |
refund.created | A refund object was created. |
refund.updated | A refund's state changed. |
refund.failed | A refund failed and the funds stay with the merchant. |
setup_intent.succeeded | A SetupIntent saved a payment method for a free-trial or deferred-start subscription checkout. |
setup_intent.setup_failed | Card verification failed for a free-trial or deferred-start checkout. |
Signature Verification
FloPay reads the Stripe-Signature header and verifies the signed raw payload with Stripe's official Stripe.webhooks.constructEvent method and the HMAC SHA-256 signing secret stored on the gateway. Missing, malformed, mismatched, or stale signatures are rejected without retry.
Inbound → Outbound Mapping
| Stripe event | Outbound Flo event | Notes |
|---|---|---|
charge.refunded | item.refunded | Maps to the refunded charge's line. |
charge.dispute.created | item.charged_back, invoice.updated, dispute.created, pre_dispute_alert.created, or (no outbound money event) | The dispute is opened. When it has no balance movement, Flo records no amount and emits no outbound money event. A previously unseen withdrawal is emitted once. A formal dispute also starts the lifecycle with dispute.created; an inquiry or early-warning state starts a Pre-Dispute Alert with pre_dispute_alert.created instead. |
charge.dispute.updated | dispute.updated, dispute.won, dispute.lost, pre_dispute_alert.updated, item.charged_back, invoice.updated, or (no new outbound) | A material lifecycle change emits the matching dispute or Pre-Dispute Alert event. Any previously unseen balance movement carried by the notification is projected as for the other dispute events. A repeated or non-material notification emits nothing new. |
charge.dispute.funds_withdrawn | item.charged_back, invoice.updated, dispute.updated, or (no new outbound) | A previously unseen provider balance movement records the positive chargeback and updates the invoice. If already recorded, Flo emits nothing new. A material lifecycle change carried by the same notification emits dispute.updated. |
charge.dispute.funds_reinstated | invoice.updated, dispute.won, or (no new outbound) | A previously unseen reinstatement records the negative movement and restores the invoice. A duplicate or movement-free notification emits nothing new, no second item.charged_back, and no item.refunded. The win behind the reinstatement arrives once as dispute.won. |
charge.dispute.closed | item.charged_back, invoice.updated, dispute.won, dispute.lost, pre_dispute_alert.updated, or (no new outbound) | FloPay projects any previously unseen balance movement. A withdrawal emits item.charged_back and invoice.updated; a reinstatement emits only invoice.updated. An already-recorded or movement-free close emits nothing new. The final outcome of a formal dispute arrives once as dispute.won or dispute.lost; a closed inquiry arrives as pre_dispute_alert.updated with closed or prevented. |
customer.created | (no outbound) | FloPay associates the Stripe customer ID with the user gateway record. |
customer.updated | (no outbound) | Stored on the gateway record. |
customer.deleted | (no outbound) | Internal gateway-record cleanup. |
customer.subscription.created | subscription.created | A subscription was created. |
customer.subscription.updated | subscription.updated, subscription.cancelled, subscription.reactivated, subscription.paused, or subscription.resumed | FloPay compares the current subscription with previous_attributes to select the transition. |
customer.subscription.deleted | subscription.expired | Stripe's deleted subscription becomes Flo's terminal expired state. |
customer.subscription.paused | subscription.paused | The subscription paused. |
customer.subscription.resumed | subscription.resumed | The subscription resumed. |
invoice.created | invoice.created | A draft invoice was created. |
invoice.updated | invoice.updated | Invoice fields changed. |
invoice.deleted | invoice.deleted | A draft invoice was deleted. |
invoice.finalized | (no outbound) | The corresponding merchant activity arrives with payment state. |
invoice.sent | (no outbound) | Stripe email activity is not a Flo lifecycle event. |
invoice.upcoming | (no outbound) | Flo waits for the real billing cycle. |
invoice.will_be_due | (no outbound) | A pre-due signal is not surfaced. |
invoice.payment_action_required | (no outbound) | Checkout handles the SCA or 3DS prompt. |
invoice.payment_failed | invoice.payment_failed | Emitted for each failed attempt. |
invoice.payment_succeeded | (no outbound) | Suppressed because invoice.paid is authoritative. |
invoice.paid | invoice.paid, plus subscription.renewed and/or item.purchased | Flo emits the invoice event, a renewal for subscription billing reasons, and purchases for non-subscription lines. |
invoice.overdue | invoice.overdue | The invoice remains collectible. |
invoice.voided | (no outbound) | No paid event is emitted. |
invoice.marked_uncollectible | (no outbound) | Stripe-side state only. |
payment_intent.succeeded | item.purchased | Suppressed for invoice-backed intents. A catalog identity must resolve from metadata or the checkout item. |
payment_intent.payment_failed | (no outbound) | FloPay records the attempt and mapped decline reason internally. |
payment_intent.amount_capturable_updated | payment.authorized | A manual-capture hold is active. Emitted once the authorized transaction is recorded. |
payment_intent.canceled | payment.authorization_voided or payment.authorization_expired | A merchant release emits the voided event and a provider expiry emits the expired event. Suppressed when no voided authorization is recorded. |
radar.early_fraud_warning.created | pre_dispute_alert.created | The warning starts a Pre-Dispute Alert. It moves no money and is delivered even before the charge is linked to a FloPay payment. |
radar.early_fraud_warning.updated | pre_dispute_alert.updated or (no new outbound) | A material change to the warning, such as it no longer being actionable, updates the alert. A repeated notification emits nothing new. |
refund.created | item.refunded | FloPay loads the parent charge and emits an item-level refund event. |
refund.updated | (no outbound) | FloPay reconciles the refund state internally. |
refund.failed | (no outbound) | FloPay reconciles the failed refund internally. |
setup_intent.succeeded | (no outbound) | The FloPay subscription start publishes the subscription.* events itself. |
setup_intent.setup_failed | (no outbound) | FloPay records the declined verification attempt internally. |
FloPay derives specific subscription transitions from the Stripe state change rather than relying only on the inbound event name.
Dispute balance movements
Stripe can repeat the same movement across overlapping lifecycle notifications. FloPay keys ledger entries and outbound events to the provider balance-movement identity, so the same withdrawal converges on one result.
The notification name does not imply another movement. Any dispute event can carry a previously unseen withdrawal or reinstatement. A lost dispute confirms its existing withdrawal and adds no new money event when that movement was already recorded; the outcome itself arrives once as dispute.lost.
Dispute and Pre-Dispute Alert lifecycle
A Stripe dispute object carrying a status FloPay classifies belongs to exactly one of two FloPay families. The formal statuses (needs_response, under_review, won, lost) are Disputes and produce dispute.* events. The inquiry and early-warning statuses (warning_needs_response, warning_under_review, warning_closed, prevented) and Radar early fraud warnings are Pre-Dispute Alerts and produce pre_dispute_alert.* events. The same Stripe event never feeds both lifecycles, and no lifecycle event carries a balance movement.
Those are the only dispute statuses FloPay classifies. A Stripe destination pinned to an old API version can still send a retired status such as charge_refunded, which belongs to neither family: FloPay emits no lifecycle event for it and retains the dispute for review. Register the destination on a current Stripe API version so disputes reach FloPay with the statuses above.
Inquiry-stage activity does not reach the same provider-neutral family on every gateway. Stripe inquiries are warnings on the dispute object rather than formal disputes, so they arrive as pre_dispute_alert.*; PayPal inquiries are Disputes API cases and arrive as dispute.* with phase: "initial", described under PayPal dispute lifecycle. Subscribe to both families when you run both gateways.
Both lifecycles are keyed to the Stripe dispute or warning identity, so overlapping charge.dispute.* notifications converge on one sequenced FloPay record. Stripe reasons and statuses are normalized to the provider-neutral values in Dispute Payload and Pre-Dispute Alert Payload; the outbound payloads never carry Stripe identifiers. A Radar warning is delivered as an alert even before FloPay can link the charge to a FloPay payment, with an empty paymentIds until a link is verified.
Recurring billing behavior
Stripe drives the recurring billing cycle for Stripe subscriptions. Renewals arrive through invoice.paid with a subscription billing reason, and FloPay emits subscription.renewed with invoice.paid. Stripe Smart Retries owns off-session retries. FloPay emits invoice.payment_failed for each reported failure and does not issue another retry.
Limitations
- Stripe decides method eligibility at runtime. An enabled method can still be absent for a buyer whose device, browser, locale, currency, or amount is ineligible.
- Apple Pay and Google Pay require HTTPS and exact hostname registration. Apple Pay also requires the association file.
enabledPaymentMethodscan reduce the session list but cannot enable a method that Stripe or FloPay did not offer.showStripe={false}removes the whole Stripe panel, including hosted card fields.- The Stripe-rendered PayPal path is unreliable in Facebook, Meta, and Instagram in-app browsers. Use direct PayPal for those surfaces.
- Gateway provider, environment, and account identity cannot change after validation.
Troubleshooting
| Problem | Fix |
|---|---|
| Stripe rejects the key | Confirm the key belongs to this account and environment and has account read access. |
| Environment mismatch | Use test keys on stage and live keys on production. Create a new gateway to move environments. |
| Activation reports missing fields | Save the publishable key, secret key, and Stripe signing secret, then test again. |
| A wallet is missing | Check device support, HTTPS, payment-method enablement, and exact domain registration. |
| A method is missing | Confirm Stripe eligibility and inspect debug output for the session. |
A credential update returns 422 | Correct the replacement and resubmit. The previous credentials remain active. |