Skip to content

Before You Charge Money: Stripe and Lemon Squeezy Billing Launch Checklist

The most misleading billing test is the easiest one:

  1. Open checkout.
  2. Use a test card.
  3. Reach the success page.
  4. See the paid feature unlock.

That proves the happy path in one browser session. It does not prove that the application can maintain the correct entitlement after delayed events, duplicate deliveries, failed renewals, cancellations, refunds, plan changes, or a webhook outage.

Before charging real money, treat billing as a distributed state machine—not a button.

Visual summary of Before You Charge Money: Stripe and Lemon Squeezy Billing Launch Checklist
Key points at a glance.

The browser’s success page is not the source of truth

A user can close the tab before returning from checkout. A redirect can be replayed. A malicious user can call a client-side “mark paid” function. A payment may still be processing.

The application should grant durable access from trusted provider state, normally confirmed through a verified webhook and, where needed, a server-side API lookup.

The success page can say, “We are confirming your purchase.” It should not be the only code path that writes plan = 'pro'.

Stripe’s webhook documentation describes webhooks as the mechanism for responding to asynchronous events such as payment confirmation, disputes, and recurring payment results. Lemon Squeezy’s webhook guide similarly recommends updating the local application from webhook events.

Define the billing state before writing handlers

Do not reduce subscription state to a single boolean such as isPaid.

Your product may need to distinguish:

  • no customer record;
  • checkout started but incomplete;
  • trialing;
  • active;
  • past due;
  • payment action required;
  • paused;
  • canceled at period end;
  • canceled immediately;
  • expired;
  • refunded;
  • disputed;
  • complimentary or manually granted access.

You do not need to expose every provider status directly to the user. You do need a deliberate mapping from provider state to product access.

Write a table before implementation:

Billing condition Product access User message Operator action
Active and paid Full plan access Normal None
Trialing Trial limits Trial end date Optional reminder
Renewal failed Grace period or restricted access Update payment method Alert after repeated failure
Cancel at period end Access until paid-through date Cancellation scheduled None
Refunded Defined by refund policy Refund confirmation Review entitlement
Disputed Restricted or reviewed Contact support Investigate

The important part is consistency. The webhook, API, scheduled reconciliation job, and interface should use the same entitlement rules.

Verify every webhook signature

A webhook endpoint is public by design. Anyone can send it an HTTP request.

Do not trust the event merely because the JSON looks like a Stripe or Lemon Squeezy payload.

Stripe requires the raw request body, the Stripe-Signature header, and the endpoint secret for signature verification. Framework middleware that parses or changes the body before verification can make valid signatures fail.

Lemon Squeezy documents signature verification using the X-Signature header.

The handler should:

  1. Read the raw body in the form required by the provider.
  2. Verify the signature with the endpoint secret.
  3. Reject invalid requests.
  4. Record the event ID and minimum useful metadata.
  5. Queue or perform idempotent processing.
  6. Return a successful response quickly after durable acceptance.

Never log the signing secret, full authorization headers, card details, or unnecessary personal data.

Return success quickly, then do durable work

Stripe recommends returning a 2xx status before complex processing that could time out. Lemon Squeezy likewise recommends storing events and processing them after responding.

Why?

A slow handler creates retries. Retries create duplicate processing. Duplicate processing can create duplicate credits, repeated emails, or conflicting status changes.

A practical design is:

  • webhook endpoint verifies and stores the event;
  • a durable worker processes stored events;
  • processing records success or failure;
  • failed events retry with backoff;
  • an alert fires after repeated failure;
  • a reconciliation job compares local state with the provider.

For a small app, the “queue” can be a database table plus a scheduled worker. It still needs locking or atomic claiming so two workers do not process the same event simultaneously.

Expect duplicate events

Webhook providers may retry when your endpoint times out or returns an error. The same business event can also arrive through related event types.

Store the provider’s event ID with a unique constraint. If an already-processed event arrives again, acknowledge it without repeating side effects.

Also make the business operation idempotent. Event-ID deduplication alone does not protect you if two different events legitimately describe the same final state.

Examples:

  • “Set subscription status to active” is safer than “add one month of access.”
  • “Ensure invoice in_123 has one local record” is safer than “create an invoice row every time.”
  • “Send the activation email once for subscription sub_123” requires its own deduplication marker.

For outbound Stripe API calls, Stripe recommends idempotency keys on POST requests so a network retry does not accidentally create a second object or operation.

Do not assume event order

Distributed events can arrive later than expected or in a different order.

A cancellation event may be followed by an update containing newer provider state. An invoice event may arrive before the related subscription record exists locally. A retried old event may arrive after a newer one.

Do not implement every event as “blindly overwrite the local row with this payload.”

Safer strategies include:

  • compare provider event creation time and local processed version;
  • retrieve the current subscription or invoice from the provider when an event lacks required context;
  • write transitions that are safe when repeated;
  • reject impossible regressions or flag them for review;
  • periodically reconcile active local subscriptions with provider state.

Stripe notes that event delivery order is not guaranteed. Design for state convergence, not a perfect sequence.

Subscribe only to events you actually handle

A webhook endpoint receiving every event type creates noise, storage cost, and unreviewed behavior.

Choose the smallest useful set for your integration. The exact events depend on whether you use Checkout, subscriptions, one-time payments, trials, invoices, or usage-based billing.

For a basic subscription product, the implementation often needs to respond to categories such as:

  • successful checkout or initial subscription creation;
  • invoice paid;
  • invoice payment failed or action required;
  • subscription updated;
  • subscription canceled or expired;
  • refund or dispute where product policy requires action.

Do not copy an event list from a tutorial without mapping each event to one product decision.

Grant access from the right event

Stripe’s subscription webhook guide explains that subscription activity is asynchronous and recommends provisioning access based on successful invoice payment and active subscription state, then revoking or changing access when status changes.

The exact policy depends on the product:

  • A trial may grant access before the first payment.
  • A one-time lifetime purchase uses a different model.
  • A failed renewal may have a grace period.
  • An enterprise invoice may be handled manually.

Write the policy explicitly and test it. Do not let an AI-generated handler infer it from event names.

Enforce plan access on the server

A pricing table may disable the button for a Free user. The API must still reject the paid operation.

Server-side enforcement should check trusted entitlement state for:

  • feature access;
  • usage limits;
  • storage limits;
  • team size;
  • API quotas;
  • exports;
  • administrator actions;
  • background processing triggered outside the main interface.

Do not trust plan: "pro" sent by the browser. Do not trust a success URL parameter. Do not use editable user profile metadata as the authority.

For metered limits, enforce the limit transactionally where possible. Two simultaneous requests should not both see “one unit remaining” and exceed the plan.

Test the full subscription lifecycle

Use provider test mode or sandbox features, but test the same routes and database logic used in production.

Initial purchase

  • successful payment;
  • payment requiring additional authentication;
  • declined payment;
  • user closes checkout;
  • webhook arrives before browser redirect;
  • browser redirect arrives before webhook;
  • user refreshes the success page;
  • duplicate checkout attempt;
  • checkout for an account that already has a subscription.

Renewal

  • successful renewal;
  • failed renewal;
  • repeated failure;
  • payment method updated;
  • webhook delayed or temporarily rejected;
  • local worker fails after accepting the event.

Plan changes

  • upgrade immediately;
  • downgrade at period end;
  • prorated charge or credit;
  • seat increase and decrease;
  • downgrade below current usage;
  • repeated click or network retry.

Cancellation and resumption

  • cancel immediately;
  • cancel at period end;
  • resume before period end;
  • expiration after cancellation;
  • cancellation from the provider portal rather than your app.

Refund and dispute

  • full refund;
  • partial refund;
  • refund after service usage;
  • dispute opened and resolved;
  • entitlement policy after each event.

Account edge cases

  • same person uses another email at checkout;
  • duplicate local customer records;
  • deleted app account with an active provider subscription;
  • workspace ownership transfer;
  • subscription belongs to the workspace rather than an individual user;
  • webhook references a local record that no longer exists.

Reconcile local state with the provider

Even a well-designed webhook system can miss an event during a deployment, credential mistake, or prolonged outage.

Run a scheduled reconciliation process that:

  1. selects local subscriptions that should be active or recently changed;
  2. retrieves authoritative provider status;
  3. compares it with local entitlement state;
  4. repairs safe discrepancies or sends them for review;
  5. records what changed and why.

Also provide an operator tool to look up a customer, subscription, invoice, event history, and local access state without editing the database manually.

The tool should not expose more personal or payment data than support staff need.

Keep test and live billing completely separate

Common launch failures include:

  • live checkout uses a test price ID;
  • the app uses a live secret with a test webhook endpoint;
  • test and live signing secrets are confused;
  • local data does not record whether an ID belongs to test or live mode;
  • production accepts a plan identifier supplied by the client;
  • an old test webhook continues calling a production route.

Create a configuration table for each environment:

  • provider mode;
  • secret key location;
  • webhook endpoint and signing secret;
  • product and price IDs;
  • customer portal configuration;
  • allowed success and cancel URLs;
  • tax and receipt settings;
  • support contact.

At deployment, log non-secret identifiers such as the mode and price IDs so you can verify configuration without revealing credentials.

Refund policy and support are technical launch requirements

Charging money creates operational obligations.

Before launch, a user should be able to find:

  • price and billing interval;
  • trial terms;
  • renewal behavior;
  • cancellation method;
  • refund policy;
  • taxes or tax handling where applicable;
  • support contact;
  • terms and privacy information.

Your support process should let you answer:

  • Did the provider charge the customer?
  • Which account or workspace received access?
  • Which webhook changed the state?
  • Did the email or receipt send?
  • Can the operation be safely retried?
  • What access should remain after a refund or cancellation?

Do not promise refunds or subscription behavior the implementation cannot reliably perform.

Before switching to live mode

Confirm all of the following:

  • webhook signatures are verified from raw request bodies;
  • event IDs are stored uniquely;
  • handlers are safe under retries and out-of-order delivery;
  • slow work runs durably after quick acknowledgement;
  • plan access is enforced on the server;
  • the browser cannot assign its own plan;
  • test and live credentials, price IDs, and webhooks are separated;
  • failed renewals and cancellations have defined access behavior;
  • refunds and disputes have defined operator steps;
  • reconciliation can repair missed events;
  • support can inspect a customer without manual database surgery;
  • terms, cancellation, refund, privacy, and contact information are available;
  • error monitoring alerts on repeated webhook and billing failures.

A billing integration is ready when money, provider state, local state, and product access converge correctly—even when events are late, repeated, or interrupted.


Related: production testing and monitoring and authentication versus authorization.

Checkout works, but you are not confident about renewals, retries, or access control? The AI App Rescue / Production Readiness service reviews billing as a complete lifecycle and tests the failure cases before real customers do.

Sources and further reading

Leave a Reply

Your email address will not be published. Required fields are marked *