Back to Knowledge
GuideMoney

Payments & Stripe

Accept payments, sell subscriptions, and monetize your app. Stripe is the industry standard - here's how to use it.

Why Stripe?

Stripe handles the scary stuff: PCI compliance, fraud detection, global payment methods, tax calculation, invoicing. You focus on your product.

PCI Compliant

You never touch card data

135+ Currencies

Sell globally

Subscriptions

Built-in recurring billing

Invoices & Tax

Automatic tax calculation

Pricing: 2.9% + 30¢ per transaction. No monthly fees. You only pay when you make money.

Types of Payments

One-Time Payments

Customer pays once, gets the thing. E-commerce, digital downloads, lifetime deals.

Use: Stripe Checkout or Payment Intents

Subscriptions

Recurring billing. Monthly/yearly plans. SaaS, memberships, newsletters.

Use: Stripe Billing + Customer Portal

Quick Setup: Stripe Checkout

Stripe Checkout is the fastest way to accept payments. Stripe hosts the payment page - you just redirect users to it.

1

Create Stripe account & get keys

Go to stripe.com, sign up, go to Developers → API Keys.

2

Add keys to .env.local

STRIPE_SECRET_KEY=sk_test_...

NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...

STRIPE_WEBHOOK_SECRET=whsec_...

3

Install Stripe

npm install stripe

4

Create checkout session API route

// app/api/checkout/route.ts

import Stripe from 'stripe'

import { NextResponse } from 'next/server'

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)

export async function POST() {

const session = await stripe.checkout.sessions.create({

mode: 'payment', // or 'subscription'

payment_method_types: ['card'],

line_items: [{

price_data: {

currency: 'usd',

product_data: { name: 'Pro Plan' },

unit_amount: 2900, // $29.00 in cents

},

quantity: 1,

}],

success_url: 'https://yoursite.com/success',

cancel_url: 'https://yoursite.com/pricing',

})

return NextResponse.json({ url: session.url })

}

5

Add checkout button

// components/BuyButton.tsx

async function handleCheckout() {

const res = await fetch('/api/checkout', { method: 'POST' })

const { url } = await res.json()

window.location.href = url // Redirect to Stripe

}

Webhooks: The Critical Part

Don't skip this!

Users can close the browser before hitting your success page. Webhooks are how Stripe tells your server "payment succeeded" reliably.

Webhooks are HTTP requests Stripe sends to your server when events happen (payment succeeded, subscription cancelled, etc.).

// app/api/webhooks/stripe/route.ts

import Stripe from 'stripe'

import { headers } from 'next/headers'

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)

export async function POST(req: Request) {

const body = await req.text()

const sig = headers().get('stripe-signature')!

const event = stripe.webhooks.constructEvent(

body, sig, process.env.STRIPE_WEBHOOK_SECRET!

)

if (event.type === 'checkout.session.completed') {

const session = event.data.object

// Grant access, send email, update DB, etc.

}

return new Response('OK', { status: 200 })

}

Key events to handle:

  • checkout.session.completed - Payment succeeded
  • customer.subscription.updated - Plan changed
  • customer.subscription.deleted - Cancelled
  • invoice.payment_failed - Card declined

Subscriptions

For subscriptions, create Products and Prices in the Stripe Dashboard, then use their IDs.

1

Create products in Stripe Dashboard

Products → Add product → Add a recurring price (e.g., $29/month)

2

Use price ID in checkout

const session = await stripe.checkout.sessions.create({

mode: 'subscription',

line_items: [{

price: 'price_1234567890', // From dashboard

quantity: 1,

}],

// ...

})

3

Enable Customer Portal

Let users manage their subscription (cancel, upgrade, update payment). Enable in Stripe Dashboard → Settings → Customer Portal.

Common Pitfalls

"Not using webhooks"

Relying only on the success_url redirect means missed payments if users close browser.

Fix: Always use webhooks to confirm payment. The redirect is just for UX.

"Test mode vs Live mode confusion"

Using test keys in production = no real money. Using live keys in dev = real charges!

Fix: Use sk_test_ locally, sk_live_ in production Vercel env vars.

"Hardcoding prices"

Changing prices requires code changes and redeployment.

Fix: Create prices in Stripe Dashboard, reference by price ID. Change prices without code.

"Not handling failed payments"

Card expires, insufficient funds - if you don't handle it, users keep access.

Fix: Listen for invoice.payment_failed webhook. Email user, retry, or revoke access.

"Exposing secret key"

Putting sk_ key in client-side code = anyone can charge your customers.

Fix: Secret key stays on server only. Only NEXT_PUBLIC_ prefix goes to client (publishable key only).

Testing Payments

Use these test card numbers (any future expiry, any CVC):

4242 4242 4242 4242Success
4000 0000 0000 9995Declined (insufficient funds)
4000 0000 0000 3220Requires 3D Secure

Test webhooks locally:

# Install Stripe CLI

brew install stripe/stripe-cli/stripe

# Forward webhooks to localhost

stripe listen --forward-to localhost:3000/api/webhooks/stripe

You're ready to monetize!

Go back to the Playbook for more guides and playbooks.

Back to Knowledge