Skip to content
Ahmad Affan
Index01Work02Services03Process04About05FAQ06Blog
Open to new projects--:--Rahim Yar Khan · UTC+5Start a project

Have something worth building?

Start a project[email protected]
Ahmad Affan

Full-Stack Developer building web apps, bots and AI agents for founders and small teams worldwide.

Services

  • SaaS platforms
  • Telegram and WhatsApp bots
  • AI voice agents
  • Websites and WordPress
  • Automation and integrations
  • Trading automation

Case studies

  • Appex Market
  • TelegramCopy
  • Appointox
  • VoxPilot
  • All work

Get in touch

  • Email
  • Telegram
  • LinkedIn
  • GitHub
  • Fiverr
Ahmad Affan.
© 2026 Ahmad AffanBack to top ↑
← All guides

SaaS

Stripe subscription billing for SaaS: webhooks, plan limits and what goes wrong

Taking the first payment is easy. Keeping access correct when cards fail, plans change and events arrive out of order is the real work.

By Ahmad Affan·Updated 28 October 2026·3 min read

Clay illustration of a credit card feeding a subscription dial

On this page

  1. Make webhooks the source of truth
  2. The events that matter
  3. Verify every webhook
  4. Assume duplicates and disorder
  5. Enforce plan limits on the server
  6. Failed payments are normal
  7. Pre-launch checklist
  8. Sources
  9. Questions

Key takeaways

  • Let Stripe’s webhooks, not the browser redirect, decide who has access.
  • Verify the Stripe-Signature header using the raw request body and your endpoint secret.
  • Stripe does not guarantee event order and can retry events, so make handlers idempotent and fetch current state when in doubt.
  • Enforce plan limits on the server from your own record of the subscription.
  • Plan for failed payments, upgrades with proration and cancellations before launch.

The checkout page is the easy part of subscription billing. The harder part starts afterwards: a card expires, a customer upgrades mid-month, a webhook arrives twice, or two events arrive in the wrong order. If your app decides access in the wrong place, you either give away the product or lock out paying customers.

I built this layer for TelegramCopy, where Stripe subscriptions set how many trading accounts a customer can connect, and the server enforces that limit. The principles below come from that kind of work and from Stripe’s own documentation.

Make webhooks the source of truth

After a customer pays, Stripe sends the browser back to your success page. That redirect is for the customer’s comfort. It can be closed, blocked or forged, so never grant access from it. Grant access from a verified webhook, and keep your own copy of the subscription state in your database.

The events that matter

EventWhat it meansTypical action
invoice.paidAn invoice was paidProvision or extend access when the subscription is active
invoice.payment_failedA payment attempt failedNotify the customer and start your recovery flow
customer.subscription.updatedPlan, status or discount changedUpdate the stored plan and recalculate limits
customer.subscription.deletedThe subscription endedRevoke paid access at the right moment
customer.subscription.trial_will_endTrial ends in about three daysRemind the customer to add a payment method

Verify every webhook

Stripe signs each event. Check the Stripe-Signature header using the official library, the raw request body and your endpoint’s secret. A common bug is a web framework that parses and re-serialises the JSON before you verify it, which changes the bytes and makes valid signatures fail. Read the raw body first. The libraries also reject events with old timestamps, which helps against replay attacks.

Assume duplicates and disorder

Stripe retries events when your endpoint does not return a success status, and it does not guarantee delivery in the order events were created. Two habits keep you safe:

  • Store processed event IDs with a unique constraint and skip any you have already handled.
  • Fetch current state from the API when an event depends on an object you have not seen yet, instead of assuming order.
  • Respond quickly with a success status, then do slow work, such as emails and provisioning, in a background job.

Enforce plan limits on the server

A plan is only real if the backend checks it. Store the plan and its limits next to the customer record, and check them whenever a user tries to add a seat, an account or a record. Do not rely on hiding a button. When a customer changes plan, Stripe handles proration on the invoice, and your webhook updates the limits. Decide in advance what happens if someone downgrades below their current usage: block the downgrade, or give a grace period.

Failed payments are normal

Cards expire and banks decline. Turn on Stripe’s automatic retry and recovery features, send a clear email when payment fails, and decide how long access continues while you retry. Showing a banner inside the product (“update your card”) usually recovers more revenue than a silent lockout.

Pre-launch checklist

  • Webhook endpoint verifies signatures and rejects stale timestamps.
  • Processed event IDs are stored, so retries are harmless.
  • Access is derived from your stored subscription state.
  • Plan limits are enforced in the API, with tests.
  • Failed-payment, cancellation and refund flows are tested in Stripe’s test mode.
  • A customer portal link lets people update cards and cancel without emailing you.
  • An admin screen shows each customer’s plan, status and last events.

If you are adding billing to a product or building one from scratch, this is part of my SaaS development service. I can also review an existing integration.

Sources

Checked on 28 October 2026. Prices and versions change, so confirm with the vendor before you commit.

  • Stripe Docs: Using webhooks with subscriptions
  • Stripe Docs: Receive Stripe events in your webhook endpoint
  • Stripe Docs: How subscriptions work

Frequently asked questions

  • For subscriptions, provision access when you receive invoice.paid and the subscription status is active, then keep the stored plan in sync with customer.subscription.updated.

  • Most often because the request body was modified before verification. Stripe needs the raw, unmodified body, the Stripe-Signature header and the correct endpoint secret.

  • Yes. Events can be retried and delivered out of order, so store processed event IDs and make handlers safe to run more than once.

  • In your backend, from your own stored subscription record, at the moment a user tries to exceed a limit. Hiding buttons in the UI is not enforcement.

Related service

SaaS development

I design and build multi-tenant SaaS platforms end to end: sign-up and billing, tenant isolation, dashboards, background jobs and the production deploy. I have built four, one of them my own (Appex Market) and three for clients, so I know what happens after launch.

Explore the service
From my work

Where I used this in practice.

SaaS built for a client
telegramcopy.com
TelegramCopy website, scrolling preview

TelegramCopy

Telegram-to-MetaTrader copy trading with Stripe billing and queued execution.

NestJSNext.jsMQL4 / MQL5Stripe
Keep reading
Clay illustration of stacked tenant cubes inside a shared platform

SaaS·28 October 2026·5 min read

Multi-tenant SaaS architecture: shared tables or a database per tenant?

The tenancy model is the one decision you can’t cheaply undo. Here is how the four common patterns compare, and how I choose between them for real products.

Read the guide

Planning something like this?

Start a project