We use cookies
Some are needed to run the site and keep you signed in. Others help us understand what’s useful through analytics. We don’t run analytics until you agree. Cookie details
Getting paid is the hard-easy part.
Stripe is the standard for SaaS subscriptions. It's powerful and secure, but the setup has fine details. This guide walks through the essentials and the pitfalls.
Go to stripe.com and sign up. You'll get two API keys: the publishable key (safe to expose in frontend code) and the secret key (never expose it; keep it in .env only).
Stripe also gives you a webhook signing secret (used to verify that webhook events are genuinely from Stripe).
Keep these in a .env.local file:
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_live_...
STRIPE_SECRET_KEY=sk_live_...
STRIPE_WEBHOOK_SECRET=whsec_...In the Stripe dashboard, create a product ("Pro Plan") and add a price ($99/month or $990/year).
You'll get a price ID (price_1A2b3c4d...). This ID goes into your database and your code.
Best practice: keep the price ID in your database alongside your plan definition. Don't hardcode Stripe IDs in your code. Always look them up from a safe source (the database or config).
When the user clicks "Subscribe," you create a Stripe Checkout session. This session is a URL you redirect the user to. They enter their payment details on Stripe (secure) and return to your site.
Server-side code (a Next.js API route) creates a session with the customer's email, the requested items (price ID), the mode (subscription), and success and cancel URLs.
When a user completes payment, Stripe sends a webhook event (checkout.session.completed). This event triggers your server to: (1) verify the subscription succeeded. (2) create a subscription record in your database. (3) activate the user's account.
Without webhooks, you might miss subscription starts (the user completes payment but your server crashes before saving). Webhooks are the reliable way.
Users will fail to pay for reasons: an invalid card, an expired card, insufficient funds. Stripe retries automatically (3 times over 5 days). But you need to handle the payment-failed event.
Listen for invoice.payment_failed and email the user: "Your payment failed. Update your payment method here: [link]."
Always give users a way to update their payment method (the Stripe customer portal).
Use Stripe test mode (keys start with pk_test_ and sk_test_). In test mode, you can subscribe without a real credit card.
Test card numbers (from the Stripe docs):
Test the full flow: subscribe → payment success → database update → verify the subscription was created.
Mistake one: exposing the secret key in frontend code. Never. Keep it in .env.local (server only).
Mistake two: not verifying webhook signatures. Always call stripe.webhooks.constructEvent() to verify.
Mistake three: relying only on client-side success confirmation. Always confirm via webhook.
Mistake four: not handling payment failure. The user's card is declined, no reminder is sent, the subscription goes dark.
Mistake five: hardcoding price IDs. Keep them in the database so you can change pricing without deploying.
Members run these on their own pages, inside the dashboard. What you paste into a check never leaves your browser.
The integration is not the hard part. The failure paths after the first charge are.
Price your offer before you build checkout