| name | stripe |
| title | Stripe |
| category | Payments & Auth |
| description | Use to accept payments — Checkout, Payment Intents, subscriptions — and to securely handle webhooks with signature verification. |
| tags | ["payments","checkout","webhooks","subscriptions","billing"] |
| official_docs | https://docs.stripe.com |
| sources | ["https://docs.stripe.com/checkout/quickstart","https://docs.stripe.com/payments/accept-a-payment","https://docs.stripe.com/webhooks"] |
| last_verified | 2026-08-10T00:00:00.000Z |
Stripe — Skillship
Accept payments online. The fastest path is Stripe Checkout (a hosted payment page); the durable
source of truth for "did they actually pay?" is a webhook you verify with a signature.
🧭 When to use this skill
- Use when: you need one-time payments, subscriptions, or a checkout flow.
- Use when: you must react to payment events (fulfill orders, grant access).
- Don't use for: storing raw card numbers yourself (let Stripe handle PCI scope).
⚡ Quickstart
1. Install
npm install stripe @stripe/stripe-js
2. Configure env (Dashboard → Developers → API keys)
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...
STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
3. Server client
import "server-only";
import Stripe from "stripe";
export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
🧩 Common recipes
Recipe: Create a Checkout Session (Next.js Route Handler)
import { stripe } from "@/lib/stripe";
import { NextResponse } from "next/server";
export async function POST(req: Request) {
const origin = req.headers.get("origin")!;
const session = await stripe.checkout.sessions.create({
mode: "payment",
line_items: [{ price: process.env.PRICE_ID!, quantity: 1 }],
success_url: `${origin}/success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${origin}/?canceled=true`,
automatic_tax: { enabled: true },
});
return NextResponse.redirect(session.url!, 303);
}
Always set the amount/price on the server from a trusted price ID — never from the request body.
Recipe: Verify a webhook (this is the important one)
import { stripe } from "@/lib/stripe";
export async function POST(req: Request) {
const body = await req.text();
const sig = req.headers.get("stripe-signature")!;
let event;
try {
event = stripe.webhooks.constructEvent(body, sig, process.env.STRIPE_WEBHOOK_SECRET!);
} catch (err) {
return new Response(`Webhook Error: ${(err as Error).message}`, { status: 400 });
}
switch (event.type) {
case "checkout.session.completed":
break;
case "payment_intent.succeeded":
break;
default:
break;
}
return new Response(, { : });
}
Recipe: Test webhooks locally
stripe login
stripe listen --forward-to localhost:3000/api/webhooks/stripe
stripe trigger payment_intent.succeeded
Recipe: Test cards
| Scenario | Card |
|---|
| Success | 4242 4242 4242 4242 |
| Requires 3DS auth | 4000 0025 0000 3155 |
| Declined | 4000 0000 0000 9995 |
🚀 Ship to production
🔐 Security & secrets
STRIPE_SECRET_KEY and STRIPE_WEBHOOK_SECRET are server-only. Never bundle them to the client.
- Verify every webhook signature — otherwise an attacker can POST fake "payment succeeded" events.
- Stripe includes a timestamp in the signature (default 5-min tolerance) to block replay attacks; keep server clocks synced (NTP). Never set tolerance to
0.
- Roll the signing secret periodically or if compromised.
🐛 Common errors & fixes
| Symptom | Likely cause | Fix |
|---|
No signatures found matching the expected signature | Body was parsed/modified before verify | Use the raw body (await req.text()), not parsed JSON |
| Webhook shows as failed / timeout | Doing heavy work before responding | Return 200 first, process async |
| Duplicate fulfillment | Stripe retried the event | Dedupe by event.id; make handlers idempotent |
| Order fulfilled but no payment | Fulfilling on client redirect | Fulfill on checkout.session.completed webhook |
Invalid API Key provided | Test vs live key mismatch | Match key mode to environment |
📚 Sources