The Complete Stripe Guide | Payments, Checkout, Webhooks, Subscriptions, Production Use
What this post covers
This is a complete guide to building a payment system with Stripe. It walks through Checkout Session, Payment Intent, Webhooks, and subscription billing with practical examples.
From the field: After migrating an in-house payment stack to Stripe, development time dropped by about 80% and payment success rates improved by about 15%.
Introduction: “Payments are hard to implement”
Real-world scenarios
Scenario 1: Security concerns
Handling card data directly is risky. Stripe provides PCI DSS compliance. Scenario 2: I need many payment methods
Integrating each one separately is complex. Stripe offers a unified API. Scenario 3: I need subscription billing
Building it from scratch is difficult. Stripe provides a subscription system.
1. What is Stripe?
Core characteristics
Stripe is an online payments platform. Key capabilities:
- Checkout: Hosted payment page
- Payment Intent: Custom payment flows
- Webhook: Event notifications
- Subscription: Recurring billing
- Multiple payment methods: Cards, Apple Pay, Google Pay
2. Installation and setup
Install
npm install stripe @stripe/stripe-js
Environment variables
STRIPE_SECRET_KEY=sk_test_...
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
Server initialization
// lib/stripe.ts
import Stripe from 'stripe';
export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: '2023-10-16',
});
3. Checkout Session
Server (API Route)
// app/api/checkout/route.ts
import { stripe } from '@/lib/stripe';
import { NextResponse } from 'next/server';
export async function POST(req: Request) {
const { priceId } = await req.json();
const session = await stripe.checkout.sessions.create({
mode: 'payment',
line_items: [
{
price: priceId,
quantity: 1,
},
],
success_url: `${req.headers.get('origin')}/success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${req.headers.get('origin')}/cancel`,
});
return NextResponse.json({ url: session.url });
}
Client
// components/CheckoutButton.tsx
'use client';
export default function CheckoutButton({ priceId }: { priceId: string }) {
const handleCheckout = async () => {
const response = await fetch('/api/checkout', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ priceId }),
});
const { url } = await response.json();
window.location.href = url;
};
return <button onClick={handleCheckout}>Checkout</button>;
}
4. Payment Intent
Server
The example below uses TypeScript and async/await. Review each part to understand its role.
// app/api/payment-intent/route.ts
export async function POST(req: Request) {
const { amount } = await req.json();
const paymentIntent = await stripe.paymentIntents.create({
amount: amount * 100, // cents
currency: 'usd',
automatic_payment_methods: {
enabled: true,
},
});
return NextResponse.json({ clientSecret: paymentIntent.client_secret });
}
Client
The client uses TypeScript, React hooks, and Stripe Elements. It fetches a clientSecret, wraps the form in Elements, and confirms the payment with error handling.
'use client';
import { loadStripe } from '@stripe/stripe-js';
import { Elements, PaymentElement, useStripe, useElements } from '@stripe/react-stripe-js';
import { useState, useEffect } from 'react';
const stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!);
function CheckoutForm() {
const stripe = useStripe();
const elements = useElements();
const [message, setMessage] = useState('');
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!stripe || !elements) return;
const { error } = await stripe.confirmPayment({
elements,
confirmParams: {
return_url: `${window.location.origin}/success`,
},
});
if (error) {
setMessage(error.message || 'An error occurred');
}
};
return (
<form onSubmit={handleSubmit}>
<PaymentElement />
<button type="submit" disabled={!stripe}>
Pay
</button>
{message && <div>{message}</div>}
</form>
);
}
export default function CheckoutPage() {
const [clientSecret, setClientSecret] = useState('');
useEffect(() => {
fetch('/api/payment-intent', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ amount: 50 }),
})
.then((res) => res.json())
.then((data) => setClientSecret(data.clientSecret));
}, []);
return (
<div>
{clientSecret && (
<Elements stripe={stripePromise} options={{ clientSecret }}>
<CheckoutForm />
</Elements>
)}
</div>
);
}
5. Webhook
Server
Verify the Stripe signature on the raw body, then branch on event.type to fulfill orders or log outcomes.
// app/api/webhook/route.ts
import { stripe } from '@/lib/stripe';
import { headers } from 'next/headers';
export async function POST(req: Request) {
const body = await req.text();
const signature = headers().get('stripe-signature')!;
let event;
try {
event = stripe.webhooks.constructEvent(
body,
signature,
process.env.STRIPE_WEBHOOK_SECRET!
);
} catch (err) {
return new Response(`Webhook Error: ${err.message}`, { status: 400 });
}
switch (event.type) {
case 'checkout.session.completed':
const session = event.data.object;
console.log('Payment successful:', session.id);
await fulfillOrder(session);
break;
case 'payment_intent.succeeded':
const paymentIntent = event.data.object;
console.log('PaymentIntent succeeded:', paymentIntent.id);
break;
case 'payment_intent.payment_failed':
const failedPayment = event.data.object;
console.log('Payment failed:', failedPayment.id);
break;
}
return new Response(JSON.stringify({ received: true }));
}
Registering webhooks
# Local testing
stripe listen --forward-to localhost:3000/api/webhook
# Production
# Stripe Dashboard → Webhooks → Add endpoint
6. Subscription payments
Creating products and prices
Create products and prices in the Stripe Dashboard or via the API.
// Create in Stripe Dashboard or via API
const product = await stripe.products.create({
name: 'Premium Plan',
});
const price = await stripe.prices.create({
product: product.id,
unit_amount: 1999, // $19.99
currency: 'usd',
recurring: {
interval: 'month',
},
});
Subscription Checkout
const session = await stripe.checkout.sessions.create({
mode: 'subscription',
line_items: [
{
price: 'price_xxx',
quantity: 1,
},
],
success_url: `${origin}/success`,
cancel_url: `${origin}/cancel`,
});
Managing subscriptions
// Cancel subscription
await stripe.subscriptions.cancel('sub_xxx');
// Update subscription
await stripe.subscriptions.update('sub_xxx', {
items: [
{
id: 'si_xxx',
price: 'price_new',
},
],
});
Summary and checklist
Key takeaways
- Stripe: Online payments platform
- Checkout: Hosted payment page
- Payment Intent: Custom flows
- Webhook: Event notifications
- Subscription: Recurring billing
- Security: PCI DSS compliance
Implementation checklist
- Create a Stripe account
- Install the SDK
- Implement Checkout
- Configure Webhooks
- Implement subscription billing
- Test
- Deploy to production
Related reading
- Next.js App Router guide
- The complete Supabase guide
- The complete Webhook guide
Keywords in this post
Stripe, Payment, Checkout, Webhook, Subscription, Backend, E-commerce
Frequently asked questions (FAQ)
Q. What are the fees?
A. Domestic cards: 3.6% + ₩50; international cards: 4.3% + ₩50.
Q. Can I use Stripe in Korea?
A. Yes. Korean businesses can use Stripe where the product is available for your entity type.
Q. How do I test?
A. Use test mode and Stripe’s test card numbers.
Q. Is it safe for production?
A. Yes. Millions of businesses worldwide rely on Stripe as a stable payments platform.