Skip to content
UdriaUdria

Library · Guide

Stripe Handles Payments. What Does Your SaaS Still Have to Build?

Stripe gives you excellent billing primitives. Your application still owns the behaviour around them.

By Roman Grigoriev5 min read

Stripe makes the first version of subscription billing look almost trivial.

Create a product.
Create a price.
Open Checkout.
Receive a webhook.

That simplicity is one of Stripe's strengths.

It can also make it difficult to see where Stripe ends and your application begins.

If you are building Stripe SaaS billing, the hardest work is often not sending money through Stripe. It is defining how your own product behaves around the billing system.

Stripe owns payment infrastructure

Stripe is very good at things your application should not recreate.

Depending on the Stripe products you use, it can provide:

  • payment collection;
  • cards and payment methods;
  • invoices;
  • subscription objects;
  • pricing primitives;
  • Checkout;
  • customer portal;
  • webhooks;
  • retries and dunning features;
  • tax-related products;
  • payment-provider infrastructure.

Your application consumes these primitives.

It still needs its own model of what they mean.

Your application needs local billing state

Suppose a user opens your settings page.

Should you fetch every billing fact from Stripe synchronously?

Usually not.

Your application needs enough local state to make product decisions reliably and quickly.

That may include:

  • Stripe customer ID;
  • subscription ID;
  • current plan;
  • status;
  • billing period;
  • scheduled cancellation;
  • seat quantity;
  • entitlement information;
  • timestamps required for reconciliation.

The exact model depends on the product.

What matters is deciding explicitly:

Which system is authoritative for which fact?

Without that decision, billing logic tends to leak across the application.

Subscription status is not the same as product access

This is one of the most important distinctions.

A Stripe subscription has states.

Your product has entitlements.

Those are related, but they are not always identical.

For example:

  • Does access stop immediately when a user schedules cancellation?
  • What happens during a payment-recovery period?
  • Does a trial grant every paid feature?
  • If a downgrade becomes effective next month, which entitlements apply today?
  • Does changing seats change permissions immediately?

The billing provider tells you what is happening financially.

Your application decides what that means for the product.

Webhooks need reconciliation, not just handlers

A webhook endpoint is not merely a set of switch statements.

You need behaviour for:

  • duplicate delivery;
  • concurrent delivery;
  • retries;
  • transient database failure;
  • stale events;
  • out-of-order lifecycle events;
  • replay;
  • unknown objects;
  • safe acknowledgement.

A subtle example is idempotency.

Recording an event ID does not necessarily mean the event was successfully applied.

We explore that in Stripe Webhook Idempotency Is Not Just Deduplicating Event IDs.

Upgrades and downgrades are product decisions

Stripe can calculate invoices and prorations.

Your product still needs to decide:

  • Which changes happen immediately?
  • Which happen next period?
  • When do entitlements change?
  • What happens to existing seats?
  • Can a customer downgrade below current usage?
  • How is the UI presented before confirmation?

These are not payment-provider details.

They are product behaviour.

Cancellation has more than one state

"Cancelled" can mean:

  • cancellation requested;
  • cancellation scheduled at period end;
  • subscription ended;
  • access removed;
  • subscription later reactivated.

A robust implementation distinguishes these states instead of flattening them into one boolean.

Otherwise teams eventually end up with code such as:

is_cancelled = true

and no reliable answer to what that means.

Failed payments need a policy

A payment can fail without the subscription disappearing immediately.

Your application needs to know:

  • what the customer sees;
  • whether access changes;
  • when access changes;
  • whether recovery happens automatically;
  • whether support is notified;
  • which local state needs updating.

Again, Stripe supplies useful infrastructure.

Your application supplies the policy.

Seats cross billing and authorisation

Seat-based SaaS is a particularly good example of the application seam.

Stripe may hold a quantity.

Your product holds:

  • members;
  • invitations;
  • organisations;
  • roles;
  • active users.

What happens when:

active members > paid seats

?

There is no universal answer.

You need a business rule.

Billing UI is still yours

Even if Checkout and Customer Portal handle substantial parts of billing, the application still usually needs:

  • pricing;
  • current plan display;
  • upgrade/downgrade entry points;
  • cancellation state;
  • trial state;
  • error state;
  • billing settings;
  • entitlement messaging.

These states need to agree with backend reality.

Tests are where the implementation becomes trustworthy

A billing implementation should verify behaviours such as:

  • duplicate event does not duplicate business effect;
  • failed processing can safely retry;
  • scheduled cancellation does not remove access early;
  • downgrade timing matches policy;
  • entitlement changes match subscription behaviour;
  • concurrent webhook delivery remains safe.

As discussed in What Does Production-Ready Mean When AI Writes the Code?, production readiness comes from handling the behaviour around the happy path.

Stripe is not the problem

This distinction matters.

Stripe is not incomplete because your application still has work to do.

No payment provider can know every product's:

  • access model;
  • organisation model;
  • feature model;
  • seat policy;
  • downgrade policy;
  • user experience.

That seam must exist somewhere.

The question is whether each team should rediscover it independently.

That is the problem Udria's first capability is intended to address.

Stripe solves payments.

The application still needs a production billing model around them.

Sources

Was this useful?

Related reading