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 and subscription 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.
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 the events in Webhook events, then save the endpoint's whsec_... value as webhookSigningSecret.
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 |
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. Merchant handlers do not need to branch on the gateway.
Subscribed Stripe Events
FloPay subscribes to the following 30 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. |
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. |
refund.created | A refund object was created. |
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, 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. |
charge.dispute.funds_withdrawn | item.charged_back, invoice.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. |
charge.dispute.funds_reinstated | invoice.updated 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. |
charge.dispute.closed | item.charged_back, invoice.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. |
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. |
refund.created | item.refunded | FloPay loads the parent charge and emits an item-level refund event. |
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 event when that movement was already recorded.
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. |