GlassKit

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 + createStripeCheckout and createStripePortal helpers
  • app/api/stripe/checkout/route.ts — POST endpoint that creates a checkout session
  • app/api/stripe/webhook/route.ts — webhook handler with signature verification
  • lib/config.tsplans — 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.tsplans:

  1. Products → Add product
  2. Name: Starter or Pro (or whatever you call your tiers)
  3. Description: copy from lib/config.ts
  4. Recurring: monthly, $9 (Starter) or $29 (Pro), USD
  5. Save and copy the Price ID (starts with price_)

Step 3: Get your API keys

  1. Developers → API keys
  2. 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

  1. Click a pricing tier on your app
  2. Use test card 4242 4242 4242 4242, any future expiry, any CVC, any ZIP
  3. Submit — Stripe redirects to your success page
  4. Watch the stripe listen terminal — you'll see events flow in:
    • checkout.session.completed
    • customer.subscription.created
    • invoice.paid

The webhook handler logs each event. If you see them in your dev console, the wiring is correct.

Useful test cards

CardBehavior
4242 4242 4242 4242Successful charge
4000 0000 0000 9995Declined: insufficient funds
4000 0000 0000 0341Successful, but charge fails after creation (test failed renewals)
4000 0025 0000 31553D 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:

  1. Settings → Billing → Customer portal
  2. Toggle Customer portal on
  3. Configure what users can do (cancel, update card, etc.)
  4. Save

Going live

Before flipping to live mode:

  1. Activate your Stripe account — fill in business details, bank account, etc.
  2. Recreate products and prices in live mode — they don't carry over from test
  3. Update price IDs in lib/config.ts — the live priceId half of each plan
  4. Create a live webhook endpoint at https://your-domain.com/api/stripe/webhook with the same events
  5. Swap env vars in Vercelsk_live_..., pk_live_..., the new whsec_...
  6. 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_SECRET matches the whsec_... from stripe 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.

On this page