Taking a payment looks like a single action and is nothing of the sort. Between a customer pressing a button and money arriving in your account, there is an authorisation, possibly a regulatory challenge in a separate browser context, an asynchronous confirmation that may arrive before or after your page has redirected, a capture, a settlement, and — in a meaningful minority of cases — a dispute months later. Building payments reliably means designing for all of it, not just the path where everything works.
This guide covers integrating Stripe properly: the object model, the payment flow and its failure paths, why webhooks rather than redirects must be your source of truth, how subscriptions and their state machine actually behave, and the operational realities of tax, disputes and reconciliation.
What you will learn
- The core objects and how they relate, so the API stops feeling arbitrary
- Why the payment intent model exists and what problem it solved
- Webhooks as the source of truth, and how to consume them safely
- Idempotency, retries and the network failures that cause double charges
- Subscription lifecycle, proration, dunning and the states that surprise people
- Disputes, reconciliation, testing and going live without incident
- What you are actually integrating
- The object model
- The payment flow
- Why webhooks are the source of truth
- Consuming webhooks safely
- Idempotency and retries
- Saving payment methods
- Subscriptions and the billing state machine
- Proration, upgrades and cancellations
- Dunning and involuntary churn
- Tax and invoicing
- Disputes and fraud
- Reconciliation and payouts
- Testing and going live
- Twelve mistakes
- A worked example: subscription checkout end to end
- Frequently asked questions
1. What you are actually integrating
Three distinct capabilities, frequently conflated because one company provides them.
Payment processing moves money: authorising a card, capturing funds, issuing refunds, handling disputes. This is the part with regulatory weight, and it is the part you should never attempt to build.
Billing decides what to charge: subscriptions, usage metering, proration, invoicing, tax calculation, dunning. This is business logic that happens to involve money, and it is where most integration complexity lives.
Money movement handles where funds end up: payouts to your account, or splits to sellers in a marketplace. Straightforward until you have a marketplace, at which point it becomes a regulated undertaking of its own.
Most products need the first two. Being clear about which you are integrating prevents the common mistake of building elaborate subscription logic in your own application when the billing product already models it — and models the edge cases you have not thought of yet.
2. The object model
The API becomes intuitive once the relationships are clear.
| Object | Represents | Note |
|---|---|---|
| Customer | A person or business you charge repeatedly | Holds payment methods, subscriptions, invoices, tax status |
| Payment Method | A card, bank debit or wallet | Attached to a customer for reuse; you never store the raw details |
| Payment Intent | One attempt to collect a specific amount | The central object of any payment; it has a lifecycle, not a result |
| Setup Intent | Saving a payment method without charging now | Used for free trials and for storing a card for later |
| Product and Price | What you sell, and how much it costs | A product has many prices: currencies, intervals, tiers |
| Subscription | A recurring agreement between customer and price | Generates invoices on a cycle; has its own state machine |
| Invoice | An itemised amount due | Created automatically by subscriptions or manually |
| Charge and Balance Transaction | A completed movement, and its effect on your balance | Where fees appear; the basis of reconciliation |
| Event | A record that something happened | Delivered by webhook; the mechanism you build against |
The relationship that matters most: a payment intent has a lifecycle rather than a result. It is created, may require action from the customer, may be processing, and eventually succeeds or fails — possibly minutes later, possibly after your user has closed the tab. Treating it as a function call that returns success or failure is the root of most broken integrations.
3. The payment flow
The modern flow exists because regulation in several major markets requires additional customer verification for many card payments. That verification happens in a context you do not control and completes asynchronously, which forced a redesign of how payments work.
The sequence, in principle:
- Your server creates a payment intent for the amount and currency, and returns a client secret to the browser. Crucially, the amount is decided server-side — never trust an amount sent from the client.
- The client collects payment details using hosted input elements, so raw card data never touches your servers. This is what keeps you out of the heaviest compliance scope.
- The client confirms the intent. At this point one of three things happens: it succeeds immediately, it fails immediately, or it requires additional action.
- If additional action is required, the customer completes a challenge — typically their bank's verification. Your page waits, or the customer is redirected away and back.
- The intent reaches a terminal state, and a webhook informs your server authoritatively.
- Your server fulfils the order when, and only when, it receives that confirmation.
The step teams get wrong is the last one. Fulfilling based on the browser's report of success means fulfilling when a customer's connection dropped mid-challenge and the payment actually failed — or, worse, failing to fulfil when the payment succeeded but the redirect never completed.
4. Why webhooks are the source of truth
Three scenarios make this concrete.
The customer closes the tab after the bank challenge but before returning to your site. The payment succeeded. Your front end never learned. Without webhooks, you have taken money and delivered nothing.
Asynchronous payment methods — bank debits and several regional methods — do not resolve immediately. The intent enters a processing state and confirms days later. There is no browser session left to inform.
A payment fails after initially succeeding. Some methods can be reversed shortly after authorisation, and disputes arrive months later. These events have no user session at all.
The rule that follows is absolute: the browser tells you what to display; webhooks tell you what happened. Show an optimistic confirmation to the customer by all means, but fulfil, provision, ship and email only in response to a verified webhook.
5. Consuming webhooks safely
A webhook endpoint is a public URL that changes your system's state, which makes it worth building carefully.
- Verify the signature on every request, using the raw request body. Frameworks that parse and re-serialise the body break verification in ways that are confusing to debug. Without verification, anyone who finds your URL can tell you a payment succeeded.
- Respond quickly, process asynchronously. Acknowledge receipt immediately and queue the work. Endpoints that do heavy processing inline time out, which causes redelivery, which causes duplicate processing.
- Handle duplicates. Delivery is at-least-once. Record processed event identifiers and ignore repeats, or make every handler naturally idempotent.
- Do not assume ordering. Events can arrive out of order. A handler that assumes the subscription-created event precedes the invoice-paid event will occasionally be wrong. Where order matters, fetch the current state rather than inferring it from event sequence.
- Return an error only when you want redelivery. A handler that throws on a business rule violation will receive the same event repeatedly for days. Log and acknowledge instead.
- Subscribe to the events you handle, and no more. Every unhandled event type is noise that obscures real failures.
The events worth handling in almost every integration: payment succeeded, payment failed, invoice paid, invoice payment failed, subscription updated, subscription deleted, and dispute created. Everything else is situational.
6. Idempotency and retries
Network failures are indistinguishable from server failures at the client. A request that times out may have succeeded. Retrying blindly risks charging twice, which is the single worst defect a payment integration can have.
The mechanism is a unique key generated by your code and sent with each request that creates something. The first request processes normally and the result is stored against that key. An identical retry returns the stored result rather than performing the action again. A request reusing the key with different parameters is rejected, which surfaces a bug in your code rather than hiding it.
Two rules make it effective. Generate the key from something stable in your own domain — an order identifier, not a random value regenerated on each retry, which would defeat the entire mechanism. And apply it to every creating operation, not only obvious payments: creating customers, subscriptions and refunds all benefit.
Alongside this, retry only on transient failures — network errors, rate limits and server errors — with exponential backoff and jitter. Retrying a declined card immediately achieves nothing except annoying the issuing bank.
7. Saving payment methods
Storing a card for future use has its own flow, and the reason is regulatory: a payment initiated by the customer and one initiated by you later are treated differently, and consent must be captured at the right moment.
Two paths. When charging now and saving for later, indicate future usage during the initial payment. When saving without charging — a free trial, or adding a card to an account — use a setup intent, which runs any required verification without moving money.
The distinction that causes production problems is between customer-initiated and merchant-initiated subsequent payments. A saved card charged while the customer is present behaves differently from one charged by your scheduled billing at three in the morning. Declaring the correct usage type when saving is what allows the later charge to succeed without a challenge nobody is present to complete.
8. Subscriptions and the billing state machine
Subscriptions look simple and contain most of the complexity in any billing integration, because a subscription is a state machine that advances on a schedule without anyone touching it.
| Status | Meaning | What your application should do |
|---|---|---|
| trialing | In a free trial | Grant access; know the trial end date |
| active | Paid and current | Grant access |
| incomplete | First payment not completed | Do not grant access; prompt to complete |
| past_due | A renewal payment failed | Usually grant access during a grace period; prompt to fix |
| unpaid | Retries exhausted | Revoke access |
| canceled | Ended | Revoke access at period end or immediately, per your policy |
| paused | Deliberately suspended | Per your product's semantics |
The architectural decision that determines how much pain you experience: do not replicate this state machine in your own database. Store the subscription identifier and a derived entitlement — does this account have access, and until when — and update it in response to webhooks. Attempting to mirror every status transition means implementing the same logic twice and reconciling two sources of truth forever.
The most common access-control bug is treating anything other than active as no access. A customer whose renewal failed on a Tuesday should usually keep working while the retries run; cutting them off immediately converts a recoverable payment failure into a cancellation.
9. Proration, upgrades and cancellations
Changing a subscription mid-cycle raises questions with no universally correct answer, which is why the platform makes the behaviour configurable and why you must decide deliberately.
Upgrades are usually charged immediately with a proration for the remainder of the period — the customer gets the better plan now and pays the difference. This is the least surprising behaviour and the easiest to explain.
Downgrades are usually applied at the end of the current period rather than immediately, because refunding the difference creates awkward accounting and invites abuse through repeated switching.
Cancellations have two sensible policies: immediate with a refund, or at period end with access retained. The second is far more common and produces fewer support conversations, but it must be communicated clearly or customers believe they are still being charged.
Whatever you choose, preview the change before applying it and show the customer the exact amount. Proration arithmetic is not intuitive, and an unexplained charge is a dispute waiting to happen.
10. Dunning and involuntary churn
A significant share of subscription cancellations are not decisions — they are expired cards and failed payments that nobody resolved. This is involuntary churn, and it is the cheapest churn to reduce because the customer still wants the product.
The mechanisms that work, in order of return:
- Automatic card updating, where the network supplies new details when a card is reissued. This alone recovers a substantial share of failures with no customer involvement.
- Smart retry scheduling rather than fixed intervals, since retry timing materially affects success rates.
- Clear, early communication. An email before expiry, and a specific message on failure explaining what to do — not a generic billing notice.
- An in-product prompt for past-due accounts, which converts far better than email alone.
- A grace period so a temporary failure does not immediately remove access and trigger a re-signup decision.
Configure the terminal behaviour deliberately: after retries are exhausted, the subscription can be cancelled, marked unpaid, or left open. Each has different implications for whether the customer can resume without re-entering everything, and the difference shows up in recovery rates.
11. Tax and invoicing
Tax is the requirement most consistently underestimated. Rates depend on what is sold, where the buyer is, where you are established, and whether they are a business. Digital services sold across borders have their own rules, and registration thresholds vary by jurisdiction and change without consulting you.
Use the platform's tax capability rather than building this. The compliance cost of getting it wrong exceeds the subscription cost by a wide margin, and the rules change frequently enough that a hand-maintained table is guaranteed to drift.
The integration points that matter: collecting and validating the customer's address and business tax identifier, deciding whether displayed prices include or exclude tax (a decision that varies by market expectation), and ensuring invoices contain the fields your customers' finance teams require. That last point generates more support requests than the tax calculation itself — business customers need a compliant invoice with their company details, and cannot process a payment receipt.
12. Disputes and fraud
A dispute occurs when a cardholder asks their bank to reverse a charge. You are notified, you may submit evidence, and the bank decides. Disputes cost a fee regardless of outcome, and a sustained high rate attracts intervention from processors.
Practical guidance:
- Collect evidence automatically at the time of purchase. Delivery confirmation, the terms accepted, the device and address used, and any support correspondence. Assembling this months later is how disputes are lost.
- Use a recognisable billing descriptor. A large share of disputes are customers not recognising a charge. This is the cheapest single reduction available.
- Make refunds easy. A refund costs less than a dispute in fees, in effort, and in your dispute rate.
- Configure fraud rules deliberately. Blocking aggressively also blocks legitimate customers; for most businesses over-blocking costs more than the fraud it prevents. Review the middle band rather than declining it.
- Track your dispute rate as a first-class metric, because processors act above a threshold and the intervention is unpleasant.
13. Reconciliation and payouts
The money in your bank account will not match the sum of your successful payments, and finance teams need to understand why. The differences come from processing fees, refunds, disputes and their fees, currency conversion, and the timing gap between capture and payout.
The object that resolves this is the balance transaction, which records each movement's gross amount, fee and net effect. Reconciliation means matching payouts to the balance transactions they comprise, and matching those to orders in your own system.
Two practices make this tractable. Store your own identifier as metadata on every payment, so every transaction can be traced back to an order without guesswork. And ingest the daily reports into your data platform rather than reconciling through the dashboard, so finance can query rather than export.
14. Testing and going live
Test mode mirrors live mode with separate keys and data, and specific test values trigger specific behaviours — success, decline, requiring verification, disputes. Use them to exercise the paths that are hard to reproduce otherwise.
The paths worth testing explicitly, because they are the ones that break in production:
- A payment requiring additional verification, completed and abandoned.
- A payment that fails after the customer has left the page.
- A webhook arriving before your own database write has committed.
- Duplicate webhook delivery.
- Webhooks arriving out of order.
- A renewal failing, retrying and recovering.
- A subscription upgraded mid-cycle, with the proration displayed.
- A dispute raised and evidence submitted.
Before going live: confirm webhook signature verification is enforced, keys are in a secret store rather than in configuration files, the billing descriptor is recognisable, refund and support processes exist, and someone is alerted when webhook processing fails. That last one is the most commonly missing and the most damaging — a silently failing webhook handler means customers paying and receiving nothing.
15. Twelve mistakes
- Fulfilling on the browser's report of success. Missed payments and unfulfilled orders.
- No webhook signature verification. Anyone who finds the URL can grant themselves anything.
- Trusting an amount sent from the client. Customers pay what they choose.
- No idempotency keys. Network retries become double charges.
- Heavy processing inside the webhook handler. Timeouts cause redelivery and duplicate work.
- Assuming webhook ordering. Handlers that break when events arrive out of sequence.
- Mirroring the subscription state machine locally. Two sources of truth, permanently diverging.
- Revoking access the moment a renewal fails. Turns recoverable failures into churn.
- Building tax logic yourself. Rules change, and getting it wrong is expensive.
- An unrecognisable billing descriptor. A large and entirely avoidable share of disputes.
- No alerting on webhook failures. Silent breakage while customers are charged.
- No metadata linking payments to orders. Reconciliation becomes archaeology.
16. A worked example: subscription checkout end to end
Consider a product with a fourteen-day trial and a monthly plan. Walking the flow shows where each concept earns its place.
Signup. The customer provides an email and card details. Because nothing is charged yet, this uses a setup intent rather than a payment intent — the card is verified and saved with consent for future merchant-initiated charges. A customer object is created with your own account identifier stored in metadata, which is what makes every later reconciliation question answerable.
Trial. A subscription is created with a trial period. Its status is trialing, and your application grants access based on a derived entitlement field — not by querying the payment provider on every request, which would put an external dependency in your authentication path.
First renewal. The trial ends and an invoice is generated automatically. If payment succeeds, an invoice-paid webhook arrives and your entitlement is extended. If it requires verification — which can happen even on a saved card — the subscription moves to incomplete, and your application must prompt the customer to complete it. This is the path most integrations forget, and it produces customers who believe they are subscribed and are not.
A failed renewal three months later. The card has expired. The invoice payment fails, the subscription moves to past_due, and retries begin. Your application keeps access for a grace period and shows an in-product prompt with a link to update the card. Automatic card updating resolves a meaningful share of these without the customer noticing at all.
An upgrade mid-cycle. The customer moves to an annual plan. Your application previews the change first, showing the exact prorated amount, and applies it only after confirmation. The immediate charge succeeds, an invoice-paid webhook arrives, and the entitlement is extended to the new period end.
A cancellation. The customer cancels. Your policy is cancellation at period end, so the subscription is marked to cancel rather than deleted, access continues to the paid-through date, and a subscription-deleted webhook arrives at that point to revoke it. Communicating this clearly at the moment of cancellation prevents the support message asking why they still have access.
A dispute six weeks later. A dispute webhook arrives. Because evidence was collected at purchase — the terms accepted, the address and device used, the access log showing product usage — submitting a response takes minutes rather than an afternoon of searching.
Every step in that sequence is driven by a webhook rather than by a browser event, and every state change flows into a single derived entitlement rather than a replicated state machine. Those two decisions account for most of the difference between an integration that runs quietly for years and one that generates a steady trickle of support tickets nobody can explain.
17. Frequently asked questions
Hosted checkout or a custom form?
Hosted checkout is faster to build, handles verification and local payment methods automatically, and keeps you furthest from compliance scope. A custom form using hosted input elements gives design control while still keeping raw card data off your servers. Fully custom collection is not worth the compliance burden for almost anyone. Start hosted; move to elements when the design constraint is real.
Should we store subscription state in our database?
Store the identifier and a derived entitlement — has access, and until when — updated by webhooks. Do not replicate the full state machine, because you will implement the same transitions twice and they will diverge. When you need detail, fetch it; when you need an access decision, use your local entitlement so an external outage does not lock out your users.
How do we handle webhooks arriving before our own data is committed?
It happens more often than expected, because the notification can arrive while your own transaction is still open. Two defences: commit your own record before confirming the payment client-side, and make the webhook handler tolerant — if the referenced order does not exist yet, requeue with a short delay rather than failing. A handler that assumes its dependencies exist will fail intermittently in ways that are difficult to reproduce.
What about testing webhooks locally?
Use the command-line tool that forwards events to a local endpoint, and trigger specific event types deliberately rather than waiting for them to occur naturally. Test duplicate delivery and out-of-order arrival explicitly, since both happen in production and neither occurs during ordinary development.
How do we support customers in many countries?
Enable the local payment methods that dominate in each market — in several regions, cards are not the primary method, and offering only cards costs conversion. Present prices in local currency, decide tax-inclusive or exclusive display per market convention, and be aware that some methods are asynchronous and some do not support refunds the way cards do. The payment element handles much of this presentation automatically if you let it.
What is the right approach for usage-based billing?
Report usage as it occurs rather than calculating a total at period end, so the platform aggregates and invoices correctly. Make reporting idempotent with your own event identifiers, because duplicate usage reports become duplicate charges. And expose usage to the customer in your product during the period — an invoice that is larger than expected with no visibility beforehand is the most common cause of usage-billing disputes.
How do we migrate from another provider?
Card data can generally be transferred between providers through a supported migration process rather than asking customers to re-enter details — start that conversation early, as it takes time. Run both integrations in parallel, moving new customers to the new provider and migrating existing subscriptions in batches. Reconcile carefully during the overlap, because two systems billing the same customer is a failure mode with immediate consequences.
What should we build first?
One payment path end to end: server-created intent, hosted collection, webhook-driven fulfilment, idempotency keys, and alerting on webhook failure. Get that correct before adding subscriptions, because every subscription problem is a payment problem with a schedule attached — and a fulfilment bug that occurs once per customer becomes a fulfilment bug that occurs every month.
Key takeaways
- Webhooks are the source of truth. The browser tells you what to display; webhooks tell you what happened.
- A payment intent has a lifecycle, not a result. Design for verification, delay and abandonment.
- Idempotency keys on every creating call. Network retries must never become double charges.
- Do not replicate the subscription state machine. Store an entitlement and update it from events.
- Involuntary churn is the cheapest to fix. Card updating, smart retries, grace periods and clear prompts.
- Collect dispute evidence at purchase time. You cannot assemble it convincingly months later.
A reliable payments integration is mostly a matter of respecting that money moves asynchronously and that networks fail at inconvenient moments. Build for the paths where things go wrong, and the path where everything works looks after itself.
Enjoyed this article?
Get more engineering insights from ELIVTECH — or talk to us about your project.
Get in touch