How-to·

Stripe subscriptions in Nuxt: pricing, checkout, and webhooks

Set up Stripe subscriptions in Nuxt 3 the right way: pricing, Checkout, webhooks, trials, taxes, emails, portal, and admin so you can launch fast.

Subscriptions bring predictable revenue, but wiring them into a Nuxt app goes wrong in small ways that cost you time and churn. The biggest mistakes we see: trusting a success URL, mixing test and live IDs, and letting the client choose any price. Here is a working baseline for Stripe subscriptions in Nuxt 3, with the practical decisions we ship in our Nuxt SaaS starter so you can move fast and still sleep at night.

1) Plan pricing tiers and trials

Design the model before you code. Your pricing rules drive navigation, access control, and analytics. Write them down so the app, Stripe, and support scripts all match.

  • Map features to tiers. Decide what Free, Starter, and Pro actually unlock. Every paid feature should live behind protected routes and server-side checks. A simple pattern: public marketing pages, an authenticated area for Free, and a premium area visible only when a subscription is active.
  • Intervals and amounts. Lead with monthly. Offer annual with a clear percentage discount, not a vague “save big.” Keep names human: Starter $12/month, Team $29/month. Avoid more than three tiers at launch.
  • Trials. Choose length (7 or 14 days are common), whether a card is required, and enforce one trial per account. If abuse is a risk, limit trials to verified email domains or one per payment method.
  • Seats vs usage. For per-seat pricing, you will pass a quantity to Checkout and keep a seat count in your database. For usage, plan on metered billing and a daily job that reports usage to Stripe.
  • International copy and currency. If you localize, write pricing copy in your translation files now. Use Intl.NumberFormat for currency display and show the currency you actually bill in.

Using a Nuxt SaaS starter kit means you can plug your tiers into a prebuilt pricing page, wire the calls to action once, and keep the offer consistent across the site and app.

2) Create products and prices in Stripe

Stripe uses Products and Prices. One Product per tier, multiple Prices for intervals or currencies.

  1. Create products. Add a Product for each tier. Match names and descriptions to what users see in-app. Use clear internal metadata like plan_key so support can search quickly.
  2. Add recurring prices. For each Product, create Prices for monthly and annual with the right currency and amount. If you offer trials, set trial_period_days on the Price or pass it in Checkout when you create the session.
  3. Use lookup keys. Set a lookup_key like starter_monthly instead of hardcoding Price IDs in code. You can then reference prices by lookup key in different environments safely.
  4. Taxes and addresses. If you collect tax, enable Stripe Tax or configure manual tax rates. Require billing address and VAT/tax IDs in Checkout when needed.
  5. Stay in test mode. Do all setup in test first. Keep a checklist of Price IDs or lookup keys to avoid mixing with live later.

In your app, keep a plans table that maps a plan_key to human labels and Stripe IDs. Suggested fields: plan_key, stripe_product_id, stripe_price_id_monthly, stripe_price_id_annual, interval, features, active. This lets you render pricing accurately and validate that any priceId coming from the client is actually one you sell.

3) Implement a subscription checkout in Nuxt

Use a server route to create a Checkout Session. Never expose secret keys in the browser. Never grant access based on a success URL.

Client flow

  • User clicks a plan button. The client calls POST /api/billing/checkout with a safe payload: plan_key (or a known lookup_key), optional quantity, and the current organization or user ID.
  • The server validates the plan against your database, resolves to a Stripe Price (monthly or annual), and creates a Checkout Session in subscription mode. Return only the session URL.
  • Redirect to the session URL. Stripe handles SCA and 3D Secure.
  • Stripe sends users back to your success_url or cancel_url. Show a friendly state, but do not activate access here. Wait for the webhook.

Server considerations

  • Associate customers. When creating the session, pass the authenticated email and a saved customer ID if you have one. Returning customers keep their payment method and invoices in one place.
  • Metadata. Include user_id or org_id and plan_key in metadata so webhook handlers can join events to your records without extra queries.
  • Quantities and seats. For seat-based plans, pass quantity to Checkout and store it on your subscription record. Update when admins add or remove seats.
  • Billing Portal. Enable the Stripe Billing Portal and add a button in your app’s billing page so customers can update cards, change plans, or cancel without support.
  • Protect content. Gate premium routes via server middleware and API guards. Hide UI affordances, but always enforce on the server too.
  • Environment config. Keep STRIPE_SECRET_KEY and STRIPE_WEBHOOK_SECRET in runtimeConfig. Separate test and live values by environment.

A Nuxt SaaS starter kit like shipahe.ad includes subscription checkout for one-time and recurring payments, swappable providers behind a small interface, user authentication, and protected pages. You focus on plans and copy, not plumbing.

4) Handle webhooks for lifecycle events

Webhooks are the source of truth for access. Create a secure endpoint, verify signatures against the raw request body, and make idempotent updates to your database.

Events to handle

  • checkout.session.completed. Read the session, capture customer and subscription, and attach the Stripe customer ID to your user or organization. Set subscription status to active or trialing based on the object.
  • customer.subscription.updated. Track status, current_period_end, cancel_at_period_end, trial_end, items.price.id, and quantity. This powers upgrade/downgrade UI, trial banners, and renewal notices.
  • invoice.payment_succeeded. Confirm continued access, increment MRR metrics, and optionally send a payment confirmation email with a link to invoices.
  • invoice.payment_failed. Mark the account past_due, show in-app notices, and email a short, actionable message with a link to update the card. Do not immediately revoke access if Stripe will retry.
  • customer.subscription.deleted. Move users to Free and remove premium capabilities. Preserve data but hide premium features.

Verification, idempotency, and retries

  • Verify signatures. Use your webhook signing secret to verify requests with the raw payload. In Nuxt, read the raw body before JSON parsing for signature checks.
  • Idempotent updates. Store processed Stripe event.id values in a table. If Stripe retries, your handler should be safe to run again.
  • Fast acks, background jobs. Do small updates inline, queue heavy work (reports, large emails) to a worker, and return 2xx quickly so Stripe stops retrying.

Keep a subscriptions table with fields like stripe_customer_id, stripe_subscription_id, price_id, status, current_period_end, cancel_at_period_end, trial_end, and quantity. If you support teams, scope subscriptions to an org_id instead of a user.

For local testing, use the Stripe CLI to forward events to localhost and trigger scenarios. A typical flow: listen, run through Checkout in test mode, and confirm your webhook updates the subscription row exactly once.

5) Receipts, dunning, upgrades, and proration

After the basics work, refine renewals, failed payments, and plan changes so customers always know what will happen next.

  • Receipts. Let Stripe email receipts automatically. If you also want in-app messages, send a transactional email on invoice.payment_succeeded with invoice links and a clear subject.
  • Dunning. Start simple with Stripe’s retry schedule. On invoice.payment_failed, show a banner and send a concise email with a secure link to your billing page or portal. Escalate tone only after the final failed attempt.
  • Upgrades and proration. Stripe prorates by default when upgrading mid-cycle. Before confirming a change, call the upcoming invoice endpoint to show the customer what they will pay now and what renewals look like. Reflect current_period_end in UI.
  • Trials to paid. Send onboarding during the trial (trial_will_end) and right after checkout.session.completed. The goal is clear “first value” before the first charge.
  • Analytics. Track plan_selected, checkout_started, checkout_completed, and subscription_activated. Look for drop-offs between pricing and Checkout, and between Checkout and activation.

Common pitfalls and how to avoid them

  • Trusting the success page. Only webhooks should activate access. Success URLs are easy to spoof.
  • Mismatched environments. Test keys with live Price IDs will fail. Keep test and live IDs and webhook secrets cleanly separated.
  • Letting the client pick any price. Resolve a server-side plan to a known Stripe Price. Reject unknown or mismatched IDs.
  • Missing access checks. Hide links in the UI, but also enforce access in server middleware and API handlers.
  • No admin tooling. Give support a way to look up users by email, see subscription status, resend emails, and apply courtesy extensions.
  • Skipping local webhook tests. Use a tunneling or CLI tool to forward events to localhost and exercise success, failure, cancel, upgrade, and downgrade paths.

Tie it together with a Nuxt SaaS starter kit

If you are weighing a Vue/Nuxt starter versus going from scratch, a ready-made Nuxt SaaS kit saves days of glue work. The shipahe.ad kit ships with user auth and protected pages, subscription Checkout flows for one-time and recurring payments, transactional email templates, an admin panel, scheduled cron jobs, and built-in analytics. Drop in your plans and copy, wire Stripe keys, and you have a working paywall with clear webhooks and a billing page that customers can self-serve from.

Key takeaways

  • Decide tiers, intervals, trials, and access rules first. Put paid features behind protected routes and server checks.
  • Create Products and recurring Prices in Stripe test mode, use lookup keys, and store mappings in your database.
  • Create Checkout Sessions from a server endpoint. Activate access from webhooks only, never from success URLs.
  • Handle core lifecycle events, verify signatures on raw bodies, and make idempotent updates to your subscription table.
  • Use the Billing Portal, transactional emails, and basic analytics to reduce churn and clarify charges.

The same plumbing powers a SaaS, a web app, or an AI tool. With a solid Nuxt starter and disciplined webhook handling, you can launch fast and charge with confidence.

FAQ

How do I test Stripe webhooks locally in a Nuxt app?

Run your Nuxt server and use a tunneling or CLI tool to forward Stripe events to your local webhook URL. Verify signatures and log each event to confirm your update logic.

Should I mark the user active after checkout success or only on webhooks?

Only on webhooks. The success page can be spoofed. Use events like checkout.session.completed and customer.subscription.updated to flip access in your database.

How do I handle free trials in Stripe with Nuxt?

Set a trial on the Price or pass a trial during Checkout Session creation. Store trial end dates from webhooks and show them in your billing UI so users know when they will be charged.

Can I switch payment providers later without rewriting my Nuxt front end?

Yes if your server abstracts checkout creation and billing operations. A starter kit that supports multiple, swappable providers helps you keep the client code stable.

What’s the best way to protect premium pages in Nuxt?

Use authentication and route middleware to check subscription status before rendering. Also lock down the server APIs behind those pages to prevent direct access.

Ready to ship your SaaS?

Everything you need is already built. Start today.
See Demo