Stripe setup
Wire the included Stripe template to your own Stripe account — checkout, webhooks, and customer portal.
GlassKit ships with a complete Stripe integration: checkout sessions, webhook handler with signature verification, customer portal, and subscription helpers. This page walks you through getting it live with your own keys.
What ships in the box
lib/stripe.ts— typed Stripe client +createStripeCheckoutandcreateStripePortalhelpersapp/api/stripe/checkout/route.ts— POST endpoint that creates a checkout sessionapp/api/stripe/webhook/route.ts— webhook handler with signature verificationlib/config.ts→plans— two-tier plan config (Starter $9/mo, Pro $29/mo) — customize for your product- Customer portal wiring — users manage their own subscriptions
The shape is opinionated: subscription-based SaaS. If you're selling one-time products, see One-time payments below.
Step 1: Create a Stripe account
dashboard.stripe.com/register — sign up with your business email. You'll be in test mode by default. Don't switch to live mode until you've completed the full setup.
Step 2: Create products and prices
Stripe distinguishes products (the thing) from prices (how much it costs). One product can have multiple prices (monthly, annually, etc.).
For each plan in lib/config.ts → plans:
- Products → Add product
- Name:
StarterorPro(or whatever you call your tiers) - Description: copy from
lib/config.ts - Recurring: monthly, $9 (Starter) or $29 (Pro), USD
- Save and copy the Price ID (starts with
price_)
Step 3: Get your API keys
- Developers → API keys
- Copy the Publishable key (
pk_test_...) and Secret key (sk_test_...)
Drop them into .env.local:
STRIPE_SECRET_KEY=sk_test_...
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...
Step 4: Update plan config with real price IDs
Open lib/config.ts and replace the placeholder price IDs:
plans: [
{
priceId:
process.env.NODE_ENV === "development"
? "price_1Abc..." // your test mode Starter price ID
: "price_1Def...", // your live mode Starter price ID
name: "Starter",
// ...
},
// ...
],
Why two price IDs per plan? Stripe test mode and live mode are separate environments — products created in test mode don't exist in live mode, and vice versa. The env-conditional avoids the classic "test webhook fired but the price doesn't exist in live" bug.
Step 5: Wire up webhooks (local dev)
Install the Stripe CLI:
brew install stripe/stripe-cli/stripe # macOS
stripe login
Forward events to your local server:
stripe listen --forward-to localhost:3000/api/stripe/webhook
The CLI prints a webhook signing secret on first run:
> Ready! Your webhook signing secret is whsec_abc123...
Drop it into .env.local:
STRIPE_WEBHOOK_SECRET=whsec_abc123...
Keep the stripe listen process running in a separate terminal while
you develop.
Step 6: Test the checkout flow
- Click a pricing tier on your app
- Use test card
4242 4242 4242 4242, any future expiry, any CVC, any ZIP - Submit — Stripe redirects to your success page
- Watch the
stripe listenterminal — you'll see events flow in:checkout.session.completedcustomer.subscription.createdinvoice.paid
The webhook handler logs each event. If you see them in your dev console, the wiring is correct.
Useful test cards
| Card | Behavior |
|---|---|
4242 4242 4242 4242 | Successful charge |
4000 0000 0000 9995 | Declined: insufficient funds |
4000 0000 0000 0341 | Successful, but charge fails after creation (test failed renewals) |
4000 0025 0000 3155 | 3D Secure authentication required |
Full Stripe test card reference.
Step 7: Customer portal
GlassKit ships with a customer portal route that redirects logged-in users to Stripe's hosted billing portal where they can update payment methods, cancel, or download invoices.
Enable it in Stripe:
- Settings → Billing → Customer portal
- Toggle Customer portal on
- Configure what users can do (cancel, update card, etc.)
- Save
Going live
Before flipping to live mode:
- Activate your Stripe account — fill in business details, bank account, etc.
- Recreate products and prices in live mode — they don't carry over from test
- Update price IDs in
lib/config.ts— the livepriceIdhalf of each plan - Create a live webhook endpoint at
https://your-domain.com/api/stripe/webhookwith the same events - Swap env vars in Vercel —
sk_live_...,pk_live_..., the newwhsec_... - Test with a real card and immediately refund yourself
One-time payments
If you're selling a one-time product instead of subscriptions, change the
mode in your checkout call:
import { createStripeCheckout } from "@/lib/stripe";
await createStripeCheckout({
priceId: "price_...",
mode: "payment", // ← was "subscription" by default
successUrl: `${origin}/thanks`,
cancelUrl: `${origin}/pricing`,
});
Stripe will create a one-time payment_intent instead of a recurring
subscription. The webhook event you care about is checkout.session.completed
(not invoice.paid).
Troubleshooting
Webhook handler returns 400. Signature verification is failing. Check that:
STRIPE_WEBHOOK_SECRETmatches thewhsec_...fromstripe listen- You're sending the raw request body to
stripe.webhooks.constructEvent(GlassKit does this — but if you've modified the handler, double-check)
No such price: 'price_...'. The price ID in lib/config.ts
doesn't exist in the Stripe environment your STRIPE_SECRET_KEY is for.
Common cause: test mode price ID with live mode key, or vice versa.
Customer portal returns 404. You haven't enabled the customer portal in Settings → Billing → Customer portal.
Checkout works but webhook never fires. stripe listen isn't running
in another terminal. The Stripe CLI is what forwards events to localhost
— without it, Stripe has no way to reach your machine.