" Back to blog Aug 12, 2026 · 4 min read

Free trial to paid: a checklist before you flip the switch

A trial-to-paid flow has a lot of small moving parts. Here's the order we check them in before trusting it with real cards.

"Add a checkout button" is the easy 10% of a trial-to-paid flow. The other 90% is making sure every step in between - account creation, the upgrade click, the payment webhook, and the license check - agrees on the same identifiers. Here's the order we check it in.

1. The user has one stable ID before checkout ever opens

Whatever ID your app assigns a user on signup needs to exist before they click "Upgrade," and it needs to travel with them through checkout unchanged. If checkout generates its own identity independent of your app's, you'll spend the first month writing scripts to reconcile the two.

2. That ID rides along in the checkout's custom data

Most payment providers let you attach custom metadata to a checkout session. Pass your internal user ID (and the email you already have on file) there, rather than trying to match purely on email after the fact - emails get typos, get changed, and aren't reliably unique across a lifetime of a customer relationship.

3. The webhook is idempotent before it's anything else

Payment webhooks retry. If "subscription activated" runs twice, it should produce the exact same end state as running once - not two activation emails, not a doubled trial period. Check this before checking anything about what the webhook actually does with the data.

4. The license check trusts the database, not the webhook's memory

Whatever ultimately answers "is this user allowed in" should read from the same table the webhook writes to - not hold its own separate cache of who's paid. Two sources of truth for the same fact is where trial abuse and false lockouts both come from.

Quick pre-launch checklist

  • Signup creates a stable user ID before any pricing page is shown.
  • That ID (and email) is attached as custom data on the checkout session.
  • The webhook verifies its signature and a timestamp window before trusting anything.
  • The webhook is safe to run twice on the same event.
  • The thing granting access reads live from the database, not a cached decision.
  • You've tested the full loop in the provider's sandbox, not just locally.

This is exactly the loop Docs Estimator's upgrade flow runs on - signup, checkout, webhook, license check.

See how it works