Stripe integration and SCA

Since 2021 Strong Customer Authentication (SCA) is mandatory for most online payments in the EU. Anyone building a payment form without SCA support sees conversion losses of 20 to 40 percent in the DACH region — the bank simply declines the transaction. Stripe Payment Intents solve this at tool level by automatically triggering 3D-Secure when the bank demands it.

Technically that means: you create a payment intent server-side, pass the client secret to the frontend, where it is confirmed with Stripe.js. The SCA prompt appears as a modal dialog (3D-Secure), the user confirms via app or SMS, then the payment goes through. The whole flow typically takes 10 to 30 seconds extra but is legally unavoidable.

Important: test SCA explicitly with Stripe test cards (4000 0027 6000 3184 forces 3D-Secure). Many teams only discover after going live that their flow breaks under SCA — for example because the modal is swallowed by a popup blocker or the success page redirects too early. Webhook listeners on "payment_intent.succeeded" are the only reliable way to confirm payment status server-side.

Recurring vs. one-time payment

One-time payments are technically simpler — card in, confirmation out, done. Recurring (subscriptions) brings a whole class of additional problems: cards expire, banks decline follow-up charges (soft decline), customers swap cards, VAT changes. Stripe Subscriptions solves most of this but requires deliberate decisions.

For one-time purchases use Payment Intents directly. For subscriptions, Subscription objects with a price ID defining frequency and amount. Watch out for pro-rata calculations on plan changes, trials, coupons and taxes (Stripe Tax or your own calculation). A self-built subscription logic quickly becomes a trap — Stripe Subscriptions are built for this and cover 95 percent of cases.

For the lifecycle you absolutely need webhooks: invoice.payment_failed, customer.subscription.deleted, invoice.upcoming. Without these events you learn nothing about failed renewals. A dunning strategy (three attempts over seven days) is standard. Stripe Customer Portal saves you building your own subscription management UI — customers there can update their card, view invoices and cancel.

Confirmation and receipts

A payment without visible confirmation creates uncertainty — the customer wonders whether the money is gone and nothing arrives. Three confirmation levels are expected in B2B contexts: a success screen right after the payment, a confirmation email within minutes, and a PDF receipt/invoice as attachment or download link.

For the success screen: show the key data (amount, recipient, transaction ID), not just a checkmark. The transaction ID is the bridge for support inquiries. With Stripe Checkout this runs automatically — with custom implementations you must build it yourself. The confirmation mail should include the PDF receipt right away, not just a download link that 404s in three months.

For invoices, certain mandatory entries are legally required in the EU (UStG §14 in Germany): full addresses of both parties, tax ID/VAT ID, invoice date, sequential number, service description, net/VAT/gross. Stripe Invoices can generate this automatically when the data is correctly captured. For a self-built solution a tax engine like Stripe Tax or Quaderno pays off — VAT logic (B2C, B2B with reverse charge, OSS for EU) is not trivial.

GDPR for payment data

Payment data is among the most sensitive personal data. Storing credit card numbers directly is not legally permitted except for PCI-DSS certified providers — and practically a disaster (liability in case of breach). The solution is tokenization: Stripe (or comparable providers) holds the card, you store only a token.

For GDPR this means: Stripe is a processor, a DPA is mandatory — Stripe provides a standard contract. For your privacy policy you must name Stripe as a recipient, flag the third-country transfer (USA) and rely on the EU Standard Contractual Clauses. Stripe is certified under the EU-US Data Privacy Framework, which legally secures the transfer.

For your own form: never store the card number, CVC or expiry date in your database — not even encrypted. Stripe Elements (or Checkout) tokenizes directly in the browser, your servers never see the data. A hidden field for the Stripe customer ID suffices for later linkage. Retention: transaction IDs and receipts must be kept for 10 years under German commercial and tax law — that is its own legal duty and overrides the GDPR "as short as possible" rule.

Error handling and retry

Payments fail for many reasons: blocked card, limit exceeded, bank decline, typo, abandoned 3D-Secure confirmation. A generic error "payment failed" is a conversion killer — the customer does not know whether to retry or use a different card.

The Stripe API delivers a "decline_code" per error with clear meaning (insufficient_funds, expired_card, do_not_honor). Map these to concrete instructions: for "insufficient_funds" — "Your card has reached its limit. Please try a different card". For "do_not_honor" — "Your bank declined the payment. Please contact your bank or use another payment method". A generic catch-all should only be the last stage.

For retry logic: for immediate decline errors (insufficient_funds, expired_card) automatic retry is pointless — it will fail again. For temporary errors (processing_error, bank_offline) a retry after 30 seconds can make sense. For recurring charges Stripe builds this in automatically (Smart Retries), for one-time charges you must implement it yourself or offer the customer a "try again" button. Webhook logging of every failure stage later helps to spot patterns — for example a sudden cluster of decline codes from a specific bank.