The first of the SaaS billing process best practices is a rule, not a pricing model: nothing in your database changes because a webhook arrived until its signature has been checked. Stripe or Paddle runs the card side. Your code owns that rule and nine more controls, from idempotent handlers to a billing runbook, and each one has a test.

The SaaS billing process best practices that matter before money moves

SaaS billing process best practices, on the app’s side, come down to ten controls: signature-checked webhooks, idempotent handlers, the full subscription lifecycle, server-side entitlements, separated test and live environments, a reconciliation check, failed-payment recovery with a grace period, a customer billing portal, scheduled reconciliation that alerts, and a billing operations runbook.

Payments and billing is one of the 13 areas a production hardening pass works through, and it holds 10 of the 123 checks. The question this area answers is what happens to an account when a payment fails, retries or arrives twice.

These are the billing subscription software best practices that live in your code rather than in the billing tool. Pricing tiers, invoice schedules and revenue reporting matter too, but SaaS subscription management best practices on that side are finance work, which this page leaves to the billing tool. If you already keep subscription billing program best practices for pricing and invoicing, keep them; the ten below sit underneath, in code.

ControlWhat it isWhy it mattersPage to open
Signature-checked webhooksYour handler proves each event came from the provider before acting on itForged events can wrongly grant paid access or change account state.webhooks security
Idempotent handlersProcessing the same event again changes nothing the second timeRepeated events can duplicate fulfillment or corrupt subscription state if processed unsafely.how to make a webhook handler idempotent
The full lifecycleEvery subscription event that changes access has a handlerMissing lifecycle handling can leave paid access inconsistent with billing.what is the subscription lifecycle in Stripe
Server-side entitlementsThe server decides what each plan may doClient-side plan labels alone do not protect paid functionality.how to enforce plan limits on the backend
Separated environmentsTest keys run only in test, live keys only in productionMisconfigured environments can process real charges during testing or accept no real payments in production.payment go-live checklist
A reconciliation checkYour records are compared with the provider’s and mismatches are fixedUndetected drift creates access and revenue-reporting inconsistencies.keep Stripe subscriptions in sync with database
Failed-payment recoveryA grace period and messages when a renewal failsPayment failures need a clear recovery path for customers and predictable access behavior.how to handle failed subscription payments
A customer billing portalCustomers update cards, view and pay invoices and, where enabled, switch plans themselvesRoutine billing changes otherwise require manual support.how to set up Stripe customer portal
Scheduled reconciliationThe comparison runs on a timer and alerts someoneA one-time check will not catch drift introduced after future changes.Keeping Stripe subscriptions in sync with the database (same page as the reconciliation check)
A billing runbookWritten steps for refunds, free access and disputesThe team needs clear procedures when a billing question becomes urgent.billing operations runbook checklist

What a subscription is in your database

A SaaS subscription is a recurring plan: the customer pays each period and keeps access while payments succeed. In your database it is five fields: the provider’s customer id, the plan, the status, the period end and whether the account is let in. Subscription billing keeps them true; my rule is to change each only on a verified provider event.

FieldWhere its value comes from
Provider customer idThe customer id Stripe returns; the Subscription object names the subscribed customer
PlanThe price the subscription is on, also on the Subscription object
StatusOne of Stripe’s eight values: trialing, active, incomplete, incomplete_expired, past_due, canceled, unpaid, paused
Current period endThe end of the period the customer has paid for, copied from the provider’s subscription record
Let in or notWorked out by your server from the status and the plan, never sent by the browser

SaaS billing, seen from the app, is the loop that keeps those five fields true while the provider does the collecting. Stripe’s own words for its half: “Stripe handles the payment retry logic, dunning, and status transitions.” The list of Stripe’s subscription statuses explains what each value means for access; unpaid, for example, comes with the instruction to revoke access.

My rule covers checkout too: the page the customer lands on after paying never changes the status. Stripe points the same way for a payment that needs the customer to authenticate: “Monitor the invoice.paid event on your event destination to verify that the payment succeeded. Before provisioning your product, confirm that the subscription is active or that the customer has active entitlements.” How that event reaches your server is the subject of what is a webhook.

Billing platform or payment provider: what the billing layer has to do

A billing system for SaaS is two halves, the provider’s and the app’s, and every job belongs to one of them. The table splits them.

JobThe provider’s sideYour app’s side
Checkout pageWith Stripe’s low risk integrations, payment details go to Stripe without passing through your serversCreates the checkout session on the server, with the price set there
Invoices and payment collectionStripe “automatically generates invoices, attempts payment collection, and manages the subscription status”Nothing to build
Retries and dunningStripe runs the retry logic and dunningA grace period and the notices your product sends (failed-payment recovery)
EventsSigns every webhook event it sendsChecks each signature and makes each handler safe to repeat
AccessStripe creates an active entitlement for each feature associated with the subscribed product when a subscription becomes activeReads the entitlement or status on the server and enforces it (server-side entitlements)
DriftHolds the record your app compares againstReconciliation, once as a check and then on a schedule
Tax as the sellerPaddle, Lemon Squeezy and Polar each call themselves a merchant of record and say they handle tax; Paddle’s docs also list invoicingNothing for tax if you use one; the event controls still apply

Buying a SaaS subscription management system moves more of the left column to a vendor. It never moves the right one. Whichever of the best SaaS billing solutions you choose, the right column stays in your code, so in my reading the best subscription billing solution for a small app is the one whose events your code handles completely. The end-to-end wiring for one provider is in how to integrate Stripe.

What goes wrong without it

Each failure below traces back to a row of the first table. Any of them can look fine in a demo and surface only once real money moves.

How secure is the Stripe payment system, and where the integration leaks

Stripe says a PCI-certified auditor evaluated it and certified it to PCI Service Provider Level 1. The part that can leak is the integration your app adds: a key committed to the repository, a webhook handler that skips the signature check, or paid features the server never checks.

That line comes from Stripe’s security page. Stripe’s integration guide adds that “PCI compliance is a shared responsibility and applies to both Stripe and your business”, and that low risk integrations “securely collect and transmit payment information directly to Stripe without it passing through your servers, reducing your PCI obligations.” So the questions left over are about your app.

Among the 21 third-party apps I audited in June and July 2026, three had a real secret permanently in git history, and one of those was a Stripe test key together with its webhook secret. Those 21 are a selected set I chose to audit, not a random sample, so the count belongs to them and is no rate for AI-built apps in general. The checkout-side leaks, such as a price the browser sets or a success page treated as proof of payment, are laid out in the ways a vibe-coded checkout leaks money.

A forged webhook upgrades an account

An account shows a paid plan, and Stripe has no payment for it. The handler acted on a request nobody proved came from Stripe, which is exactly the risk in the first table row. Stripe’s webhook docs describe it in plain terms: “Without verification, an attacker could send fake webhook events to your endpoint to trigger actions like fulfilling orders, granting account access, or modifying records.” The endpoint is public by design, and the signature check is how the handler tells Stripe’s events from anyone else’s. The fix in depth is under webhooks security.

The same event arrives twice and the effect happens twice

A customer gets two welcome emails, or a credit pack lands twice. Nothing was forged; the provider sent the event again, and the handler ran its side effects again. Stripe is direct about it: “Webhook endpoints might occasionally receive the same event more than once.” A retry after a slow response does the same thing, since a timed-out delivery is logged as an error and Stripe can retry failed live deliveries; two deliveries running at once can also both pass a naive “already processed?” check before either writes. The pattern that holds is in how to make a webhook handler idempotent.

The card fails and the app never notices

A renewal is declined. The app either keeps showing Pro long after the money stopped, or it locks the customer out with no message and no way to fix the card. Both come from the same gap: nothing in the app listens for the failure. Stripe’s subscription docs say that “When a recurring subscription payment fails, listen for invoice.payment_failed.” What the access rule should be during the retries, and what the customer should see, is in what your app should do when a subscription payment fails.

The database says Pro and the provider says canceled

Support finds a customer who canceled last month and still has full access, or a paying customer stuck on the free plan. The two records drifted and nothing compared them. Stripe can retry failed live webhook deliveries for up to three days and does not guarantee event order, so a missed or late event leaves the gap open. Start with Stripe webhooks failing, then a canceled Stripe subscription that still has access, and for the lasting fix, keep Stripe subscriptions in sync with database.

The free plan can call the paid endpoint

A free user opens the browser’s network tab, copies the request the Pro button sends, and it works. The plan check lived only in the interface, which hides a button but never stops a request. The server has to read the plan for itself on every paid action. An app that gives away paid access walks through how this happens; the server-side pattern, usage limits included, is the server-side entitlements control below.

Test keys in production, live keys in staging

Real customers pay and nothing unlocks, or a staging test charges a real card. Keys and secrets were copied between environments, and one landed in the wrong place. One detail catches people: webhook signing secrets are per endpoint and separate from API keys, so the same URL has different test and live secrets. Swapping the API key alone leaves every live event failing its signature check. The switch itself is in moving Stripe from test mode to live; the wider check is the payment go-live checklist.

The total the app stored is not the total the customer saw

In one app I audited, a CRM added proposal totals in floating point, so the amount stored could differ from the figure the customer was shown. Stripe’s own API takes whole numbers in the smallest unit: “All API requests expect amount values in the currency’s minor unit”, so 1000 charges 10 USD. The lesson I take from it: money the app adds up itself needs an exact type, integer cents or a decimal, or the database and the invoice can tell two stories. The column types to check are in the database review checklist.

The ten controls, one by one

1. Every webhook is signature-checked before anything changes

The control: verify the payment provider’s signatures before processing webhook payloads. With Stripe, the recommended route is the official library’s verification call, fed the endpoint’s whsec_ secret and the request body exactly as it arrived, because “Stripe requires the raw body of the request to perform signature verification.” The test: confirm valid events succeed and invalid or altered payloads are rejected. How to send those events locally is in how to test webhooks; when real events fail the check, why a Stripe webhook signature check fails covers the usual causes.

2. Every webhook handler is safe to repeat

The control: make every webhook handler safe to repeat, including concurrent delivery and its downstream side effects. Stripe’s suggestion is to log the event IDs you have processed and skip the ones already logged. Under concurrency, that log needs a unique constraint the database enforces, so two copies racing in cannot both pass the check. Side effects such as emails and credits sit behind the same guard. The test: replay and concurrently deliver events; confirm one intended business effect.

3. The whole billing lifecycle is handled, not just the first payment

The control: handle creation, updates, cancellation, payment failure, refunds, and disputes, including delayed or out-of-order events. The status values to map are the eight in the subscription table above. Events can arrive out of order, and Stripe’s docs warn: “Don’t use created to determine event order or whether you’ve already processed an event.” Fetch the current object from the API when an event looks stale. The test: test the relevant lifecycle transitions and verify the final account state.

4. Paid-plan permissions are enforced on the server

The control: enforce paid-plan permissions and usage limits in backend actions. Every route, server action or database function that does paid work reads the account’s plan or entitlement itself, from your database or the provider, and refuses when it does not match. A hidden button and a disabled menu item are decoration on top of that. Usage limits get the same treatment: count on the server, refuse on the server. The test: attempt paid operations with free, expired, and valid paid accounts.

5. Test and live credentials are isolated by environment

The control: verify that test and live credentials stay isolated, each environment holding only its own. That covers the secret key, the publishable key, the webhook signing secret and any price IDs your code holds. Read each environment’s variables and name where every value came from. A staging deploy holding a live key is a defect even if nothing has charged yet. The test: inspect provider accounts and run isolated test-mode transactions.

6. A reconciliation check finds and corrects the drift

The control: compare the application’s billing records with the payment provider and correct mismatches. In practice that is a script that lists active subscriptions from the provider, lists paid accounts from your database, and reports three groups: paying but not let in, let in but not paying, and plan or period end disagreeing. Corrections go through the same code path a webhook would use, so the fix is logged like any other change. The test: seed mismatched records and verify they are identified and reconciled correctly.

7. Failed payments have a grace period and dunning messages

The control: implement grace periods and dunning messages consistent with the product’s billing rules. Stripe’s retry settings come first: “Stripe recommends using Smart Retries, but you can also create a custom retry schedule.” Stripe doesn’t retry when “No payment methods are available” or “The issuer returned a hard decline code”, and after a hard decline “we can’t retry invoice payment without a new payment method.” When recovery fails, you choose whether the subscription is canceled, marked unpaid, left past-due or, if eligible, paused. Your grace period has to agree with that choice. The test: simulate payment failure, notice delivery, recovery, and grace-period expiry.

8. Customers can manage billing in a portal

The control: connect Stripe Customer Portal or the existing provider’s equivalent for invoices, card changes, and supported plan management. The portal session is created on your server for the signed-in customer only, never from an id the browser supplies. Whatever the customer changes there comes back to your app as events, so the portal leans on controls 1 to 3 to be correct. The setup is in how to set up Stripe customer portal. The test: test an authenticated customer’s portal access and resulting updates in the application.

9. Reconciliation runs on a schedule and alerts the team

The control: run recurring reconciliation and alert the team to billing discrepancies. The one-off check from control 6 becomes a scheduled job, and its output goes somewhere a person reads: an email, a chat channel, a ticket. A job that finds drift and writes it to a log nobody opens is the same as no job. Keep the counts it reports, so a sudden jump after a deploy stands out. The test: execute the scheduled job against test mismatches and verify its alert and output.

10. There is a billing operations runbook

The control: document refunds, complimentary access, and dispute handling in the application’s payment setup. Each procedure names the button or command, the effect on the account in your app, and who on the team has the provider permissions to do it. A refund that returns money but leaves access on, or a free month granted in the database but not in the provider, is what the runbook prevents. The test: walk through the procedures in test mode and identify the responsible account permissions. A template is in billing operations runbook checklist.

Cardholder data: what your app may store, and what it must never touch

A cardholder is the customer a payment card is issued to, or anyone authorized to use it. With Stripe’s low risk integrations, card details skip your servers, and your app may keep what Stripe returns, such as card type, last four digits and expiry date. A merchant may not keep the card verification code after the purchase is authorized.

The PCI Council’s glossary defines a cardholder as the “Customer to which a payment card is issued, or any individual authorized to use the payment card.” The everyday sense, the name printed on the card, is the same person seen from the shopper’s side.

For what payment card data you are allowed to store, Stripe’s integration security guide is specific. Stripe returns “the card type, the last four digits of the card, and the expiration date”, says “This information isn’t subject to PCI compliance, so you can store any of these properties in your database”, and adds that “you can store anything returned by our API.”

The payment card data you are not allowed to store after authorization starts with the card verification code. The PCI Council’s answer on storing card verification codes reads: “It is not permitted to retain card verification codes once the specific purchase or transaction for which it was collected has been authorized.” The one exception it names is for “issuers or those companies supporting issuing services with a legitimate issuing business need.” The code checked in card code verification is what the glossary calls the card verification code, “the three- or four-digit value printed on the front or back of a payment card.”

Card fieldMay your app keep it?Source
Card type, last four digits, expiry dateYesStripe’s integration security guide
The provider’s customer and payment method idsYes: Stripe says you can store anything its API returnsStripe’s integration security guide
Full card numberNot with Stripe’s low risk integrations: it never passes through your serversStripe’s integration security guide
Card verification codeNo, once the transaction is authorizedPCI Security Standards Council FAQ

Which self-assessment questionnaire applies to you, and the full PCI DSS requirements, are a separate subject. Stripe says it analyzes your integration and tells you “which PCI validation form to use.”

Taking card payments raises the bar: what ecommerce security means for a SaaS

Ecommerce security for a SaaS with a hosted checkout splits in two. The provider carries the card itself. The app carries the four properties the store guides list, privacy, integrity, authentication and non-repudiation, through its access control, its data checks and a trail of verified billing events.

The ecommerce guides from BigCommerce and Astra each give those four properties a heading of their own. Here is where each one sits for a SaaS, as my reading. Privacy and authentication are access control, the ground the authentication checklist covers. Integrity is application security and the checks on your data, the ground of web app security. Non-repudiation is this area: verified, deduplicated billing events in your database, backed by the provider’s own records.

For a SaaS, e-commerce cyber security is mostly application security, because security in e-commerce sits wherever the buyer’s account and money pass, and with a hosted checkout that is your login, your API and your webhook endpoint. Electronic commerce security for the card itself, the details typed into the payment form, is the part a hosted checkout hands to the provider; secure electronic commerce for a SaaS is everything after that hand-off.

The e-commerce security issues BigCommerce’s guide lists as threats include phishing, malware, SQL injection, cross-site scripting, e-skimming, DDoS, brute force and bots. A hosted checkout takes the card form off your pages, so e-skimming of card details moves to the provider; that is my reading, resting on Stripe’s “without it passing through your servers” line. Injection, cross-site scripting, brute force and bots stay with the app, under the access and data controls named above and the wider SaaS security checklist. The card side that stays with the app is the table above, and the e-commerce security solutions that matter for a SaaS are a hosted checkout plus the ten controls.

How to verify the whole area in an afternoon

Verifying the billing area takes ten tests, one per control, and about an afternoon on a small app, as my working estimate: a test-mode charge, an altered webhook, a resent one, the relevant lifecycle transitions, a paid call from a free account, a seeded mismatch, the scheduled job’s alert, a failed payment, a portal change and a runbook walk-through.

Expect to write most of these tests fresh. In my June and July 2026 audits, only 1 of the 21 third-party apps was credited with a real test suite, and even it skipped sign-up, login and the payment webhook. I picked those apps to audit; they are not a random draw, so that 1 in 21 describes them and is no rate for AI-built apps at large.

My working order for a one-person team, each item pointing back to the test written under its control:

  1. 01 Control 5, environments: confirm the test keys and the live keys are each set only in their own environment, then run one test-mode transaction in isolation. Record where each key lives.
  2. 02 Control 1, signatures: send one valid event and one with an altered payload. Record both requests and both responses; only the valid one may change anything.
  3. 03 Control 2, repeats: deliver the same event twice in a row and twice at once, each delivery freshly signed by Stripe. Record the event id and the account state before and after; expect one effect.
  4. 04 Control 3, lifecycle: run the transitions your product uses (trial to active, active to past due, canceled) and read the final account state after each.
  5. 05 Control 4, entitlements: call one paid operation as a free, an expired and a paid account. Record the three responses.
  6. 06 Control 7, failed payments: take one subscription from a failed payment through the notice, the recovery and the end of the grace period. Record what the customer saw at each step.
  7. 07 Control 8, portal: sign in as a customer, change the card or plan in the portal, and confirm the app shows the change.
  8. 08 Control 6, reconciliation: seed a few mismatched records and confirm the check finds and corrects each one.
  9. 09 Control 9, schedule: run the scheduled job against those test mismatches and confirm the alert arrives with the right output.
  10. 10 Control 10, runbook: walk each procedure in test mode and write down which account permissions it needs.

For the repeat test, a saved request replayed later proves nothing: Stripe’s libraries have “a default tolerance of 5 minutes between the timestamp and the current time”, so the old signature is refused before your handler runs. A resend is signed again, since “Stripe generates the timestamp and signature each time we send an event to your endpoint.” Use Resend on the event in the Dashboard, or run stripe events resend <event_id> --webhook-endpoint=<endpoint_id> from two terminals at once for the concurrent case.

The other areas of an app follow the same shape in the full production readiness checklist. If the app is not taking money yet, the conditions for the first card are in what must be true before you turn on payments.

Where the sprint stops

The Production Hardening Sprint has edges, and billing work meets several of them. Formal third-party certifications and independent audit opinions are separate from these engineering deliverables. The app’s current framework and hosting setup are the starting point; components are refactored or replaced where the production work requires it. Building new product features or modules, completing unfinished core features or business workflows, and rebuilding core functionality that does not yet perform its intended job sit outside it; new features and completing unfinished core workflows are separate work. Hosting, paid tools, and API usage remain in your accounts, and any required third-party costs are explained before they are enabled.

Where the sprint does this

Area 5 of the sprint is these ten controls, 10 of its 123 deliverables. We deliver each one as the control line under its heading above describes and verify it with the test written there. The customer portal is one of the supporting interfaces the listed controls need, billing self-service in the scope’s words. Every result goes into the production readiness report with its verification evidence; the report accounts for all 123 IDs, keeps failures visible until resolved and explains genuine non-applicable items. The ten items are listed under area 5 of the published scope.

Common questions about billing for a subscription app

What is e-commerce security?

E-commerce security is the set of controls that keeps an online sale private, unaltered, tied to a real customer and provable afterward: the privacy, integrity, authentication and non-repudiation that store guides list. With Stripe’s low risk integrations, the card details go to Stripe without passing through the app’s servers, so what a SaaS carries in its own code is those four properties, while Stripe holds the card.

Is it allowed to store the CVV under PCI compliance?

No. The PCI Council’s FAQ forbids keeping the card verification code once the purchase or transaction it was collected for has been authorized, and it allows an exception only for issuers and companies supporting issuing services with a legitimate issuing business need. With Stripe’s low risk integrations, the card details, the code among them, never pass through your servers, so there is nothing to store.

How does SaaS billing work?

SaaS billing works as a loop. The customer pays on the provider’s payment page, which with Stripe Checkout is “either embedded on your site or via a redirect to a Stripe-hosted page”; the provider sends a signed event; your app verifies it and updates the subscription fields; renewals and failed payments come back the same way, as events.

What is the best payment processing solution for SaaS?

No single product is best for every SaaS; the real choice is between a payment processor such as Stripe and a merchant of record such as Paddle, Lemon Squeezy or Polar. With a merchant of record, the customer is buying from that company, and each of the three says it handles tax: Paddle says “We take care of payments, tax, subscriptions, and metrics”, Lemon Squeezy names “collecting sales tax, processing refunds and chargebacks, and ensuring PCI compliance”, and Polar says “we handle all international tax compliance.” Either way, which payment gateway you pick matters less than the wiring, and the ten controls above apply to both.