Everything you need to build a Stripe integration that holds up in production: how payments and the Stripe object model work, Payment Element and Checkout, Billing and subscription lifecycle, usage-based pricing, Connect for platforms and marketplaces, webhooks and idempotency, reconciliation, dunning, Radar fraud rules, PCI scope reduction, and what a migration from another processor actually involves. No hand-waving, no omissions.
Stripe is the most complete payments platform available, and that completeness is also its main complexity risk. There are multiple ways to accept a payment, multiple product surfaces for subscriptions, and an event model that requires careful handling at production scale. This guide is how we think about and implement each layer, in the order you typically encounter them.
Before writing a line of integration code, it is worth understanding how Stripe models money movement, because the object hierarchy shapes every decision downstream. The three core objects are the Customer, the PaymentMethod, and the PaymentIntent.
A Customer is an identity in Stripe that owns payment methods and has a billing history. A PaymentMethod represents a stored payment instrument: a card, a bank account, a wallet. A PaymentIntent is a record of an attempt to collect money, and it is the object that drives the payment flow. Everything else, Subscriptions, Invoices, Charges, Transfers, is built on top of these three.
One consequence of this model matters more than anything else in a first integration: never fulfil an order based on a browser redirect. Redirects can fail, be refreshed, or be faked. The correct trigger for fulfilment is the payment_intent.succeeded webhook delivered server-to-server by Stripe. This is where most first integrations get it wrong.
Stripe offers two ways to collect a payment on the frontend. The Payment Element is an embeddable React component you mount inside your own page, giving you full control over the surrounding UI while Stripe handles the sensitive card fields and the authentication flow. Checkout is a hosted page on Stripe's domain that you redirect to, requiring almost no frontend work in exchange for less flexibility in design.
The decision between them is usually about how much control you need and how much PCI scope you want to accept. Both options use Stripe's iframe to handle card data, so neither increases your PCI scope meaningfully, but Payment Element demands more frontend code and a more careful webhook implementation. For most SaaS products, Payment Element is the right call because the design and conversion control is worth the extra work.
payment_intent.succeeded, payment_intent.payment_failed, and charge.dispute.created at minimum.The idempotency key rule. Every Stripe API call that creates or modifies an object should include an Idempotency-Key header. Use a stable identifier derived from the thing you are creating, such as your internal order ID, not a random UUID generated at request time. That way a retry from a network failure or a server crash is safe.
Stripe communicates asynchronously through webhooks: HTTP POST requests to an endpoint on your server, carrying a JSON payload describing an event. The webhook is the canonical source of truth for payment state. If you do not process it, you do not know what happened.
Stripe guarantees at-least-once delivery, which means the same event can arrive more than once, particularly after your server returns a 5xx or times out. Your webhook handler must be idempotent: processing the same event twice must produce the same result as processing it once. In practice, this means storing the Stripe event ID in your database before doing any work, and checking for it before processing any subsequent delivery of the same event.
customer.subscription.trial_will_end (remind the user), invoice.payment_failed (start the dunning sequence), customer.subscription.updated (sync plan changes to your database), and customer.subscription.deleted (revoke access). Missing any of these creates a state where Stripe and your system disagree about what a customer has access to.Verify every incoming webhook using Stripe's signature verification before processing it. The signature is in the Stripe-Signature header, and Stripe's SDK provides a constructEvent method that validates it against your webhook signing secret. Skipping this check means any actor who discovers your webhook URL can send fake events.
Return a 200 immediately after verifying the signature and queuing the event for async processing. Do not do the work synchronously in the HTTP handler: Stripe's retry logic treats any response that takes more than 30 seconds as a failure, and your processing might take longer than that.
Stripe Billing adds a layer of objects on top of the core payment primitives: Products, Prices, Subscriptions, and Invoices. A Product is what you sell; a Price defines the terms (flat rate, per seat, usage-based, one-time). A Subscription links a Customer to a Price and generates Invoices on a schedule. An Invoice is the charge attempt.
The benefit of using Billing over building your own subscription logic is that Stripe handles the scheduling, the proration on plan changes, the invoice PDF generation, the customer email receipts, and the retry logic on failed payments. The cost is that your database needs to stay in sync with Stripe's representation of each subscription, which requires careful webhook handling.
When a customer upgrades or downgrades mid-cycle, Stripe calculates proration by default: the remaining value on the old plan is credited, and the new plan is charged for the remainder of the cycle. You can control the proration behaviour when you update the subscription, either charging immediately, deferring to the next invoice, or disabling proration entirely. The right choice depends on your pricing model and what your customers expect.
Stripe supports trial periods at the Price level, which applies them automatically to every new subscriber, or at the Subscription level for custom overrides. During a trial, Stripe can either collect a payment method up front with no immediate charge (the default for most SaaS) or skip the payment method and require it only when the trial ends. Requiring a card up front reduces trial-to-paid friction at the cost of fewer trial starts. The data on which approach converts better varies significantly by product and price point.
Usage-based pricing, also called metered billing, charges customers based on how much they use rather than a flat rate. Stripe supports this through Billing Meter, which accepts usage event records and aggregates them into invoice line items at the end of each billing period.
The implementation pattern is: your application sends usage events to Stripe's Meter Events API whenever a billable action occurs. At the end of the billing period, Stripe aggregates the events according to the aggregation strategy you configured (sum, max, or last value) and generates the invoice line item from the result. Your database does not need to maintain a running total for billing purposes; Stripe is the source of truth for usage.
Idempotency on usage events. Meter Event records accept an identifier field. Set it to a stable ID for the specific action (for example, your internal event ID). If you send the same event twice with the same identifier, Stripe deduplicates it. Without this, a retry from a network failure doubles the usage count and overcharges the customer.
Involuntary churn, customers who stop paying because their card expired or was declined rather than because they chose to leave, is typically 20 to 40 percent of total churn for a SaaS with monthly billing. Most of it is recoverable if you build the right sequence. Stripe provides Smart Retries, which uses machine learning to time retry attempts at moments when approval rates are highest. On top of that, you should build a dunning sequence in your own product.
A basic dunning sequence looks like this: on invoice.payment_failed, send the customer an email with a link to update their payment method. Wait three to four days and retry. If the second attempt fails, send a second email with a stronger subject line. Retry again after another three days. If the third attempt fails, downgrade the customer to a grace state rather than cancelling immediately, send a final notice, and cancel after seven days if no action is taken. The grace state matters: it preserves the account and the relationship while the customer sorts out their card, and it avoids the friction of a cancellation and reactivation flow.
Stripe's Billing customer portal handles the payment method update flow out of the box if you want to avoid building it yourself. It is a hosted page on Stripe's domain, similar to Checkout, that customers land on from your dunning email link.
Stripe Connect is the product surface for platforms and marketplaces where money flows through your application to other businesses or individuals. If you take a booking fee and pass the rest to a service provider, or charge a buyer and split the payment between a marketplace and a seller, you are building a Connect integration.
There are three Connect account types and the choice has real consequences. Standard accounts have their own Stripe dashboard and handle their own disputes and refunds; your platform has limited control but minimal compliance burden. Express accounts use a Stripe-hosted onboarding flow and a simplified dashboard; your platform has more control and Stripe handles most compliance. Custom accounts give your platform full control over the experience and the UI, but you take on significantly more compliance responsibility. Most marketplace builds start with Express.
Stripe holds funds in a connected account's balance until a payout runs. The payout schedule can be automatic on a daily, weekly, or monthly cadence, or manual triggered by your platform code. For platforms where timing of payouts is a product feature (for example, an on-demand payment option), manual payouts are the right choice. For most marketplaces, automatic weekly or monthly payouts are simpler and reduce support volume.
Reconciliation is the process of confirming that what Stripe shows as paid matches what your database shows as owed and received. At small volumes it is manageable manually; at any real scale it requires a pipeline. The raw material is Stripe's Balance Transactions API, which lists every movement of funds in your Stripe account with a type, amount, fee, net amount, and the linked charge or payout.
A basic reconciliation pipeline: pull Balance Transactions from Stripe on a nightly schedule, store each one in your data warehouse keyed by its Stripe ID, join against your internal order records, and report any mismatches. Mismatches typically fall into four categories: charges in Stripe with no matching order in your database (failed webhook processing), orders in your database with no matching charge in Stripe (payment intent created but never confirmed), amounts that do not match (currency conversion or fee calculation error), and timing differences that resolve within a few days.
Payouts are not revenue. A payout moves money from your Stripe balance to your bank account. It is not a new transaction; it is the settlement of transactions that already happened. Many first implementations treat the payout event as revenue recognition, which creates a mismatch with accrual accounting. Revenue should be recognised on the charge.succeeded or invoice.paid event, not on the payout.
Stripe Radar is the fraud detection layer built into every Stripe integration. Out of the box it blocks transactions that match known fraud patterns, using machine learning trained on the transaction volume across all of Stripe's merchants. For most businesses the default rules are a reasonable starting point. For businesses with a specific fraud profile, custom Radar rules let you add logic on top of the defaults.
Radar rules are written in a simple boolean expression language and evaluated in order. A rule can block a charge, allow it (overriding a default block), or request a 3D Secure authentication step. Common custom rules: block charges from high-risk countries you do not serve, require 3DS for charges above a threshold amount, block cards that have appeared in too many declined transactions in the last hour, and allow charges from verified business customers in your database even when the default model flags them as suspicious.
Review your dispute rate and the false positive rate (legitimate charges that Radar blocks) on a monthly basis. A dispute rate above 0.5 percent is a signal to tighten rules; a meaningful volume of customer complaints about blocked legitimate payments is a signal to loosen them or add allow-list rules for known good customers.
The Payment Card Industry Data Security Standard (PCI DSS) defines the security requirements for any system that stores, processes, or transmits cardholder data. Stripe's integration products are designed specifically to minimise your PCI scope, meaning the surface of your systems that falls under PCI requirements.
When you use Payment Element or Checkout, the card number, expiry, and CVV are entered directly into Stripe's iframe or hosted page and never pass through your servers. Your server only ever sees the PaymentIntent ID and the token Stripe returns. This qualifies you for SAQ A, the simplest self-assessment questionnaire, which requires roughly 20 controls rather than the hundreds required for SAQ D (full card data storage).
What you must not do: log raw card data, store the full PAN in your database, pass card details through your backend even briefly, or use Stripe in a way that causes cardholder data to touch your network. These are disqualifying for SAQ A. Stripe's own documentation is clear on the boundary; the integration patterns in this guide stay within it.
Migration from an existing payment processor to Stripe is achievable without interrupting active subscriptions, but it requires careful sequencing. The two hard problems are: migrating stored payment methods (card details that live in your old processor's vault) and migrating active subscriptions (recurring charges that your old processor is running on a schedule).
Stored card migration requires your current processor to export encrypted card data in a format that Stripe's migration team can import into their vault. Not all processors support this, and the ones that do require a formal data transfer agreement. Stripe has done this with most major processors and has a documented process. The alternative is to ask customers to re-enter their card, which is simpler to implement but introduces significant churn risk on the re-entry step.
Subscription migration: once card data is in Stripe, you create matching Customer and Subscription objects with billing_cycle_anchor set to align the charge date with your old processor, so customers are not double-charged or under-charged during the transition. Run the new and old systems in parallel for at least one full billing cycle, reconcile the output, and cut over only when the two match. Set a cutover date, cancel subscriptions on the old processor the day before their next renewal, and let Stripe own the next cycle.
The migration audit covers: which processor you are leaving, whether it supports encrypted card export, the count and shape of your subscription plans, any non-standard billing logic (custom trial lengths, mid-cycle credits, multi-currency plans), and your cutover risk tolerance. That shapes the migration plan before any code is written.
That is the full implementation picture. When you want it applied to your product, the next step is a free audit: we look at what you have or what you are planning, map the gaps, and tell you what to build and in what order.
A free audit of your setup, the gaps mapped, and a fixed scope to close them. Findings in a week.
Get a free Stripe audit