stripe

Use to accept payments — Checkout, Payment Intents, subscriptions — and to securely handle webhooks with signature verification.

Works with

Claude CodeCursorCodex CLIGitHub CopilotGemini CLI
---
name: stripe
description: Use to accept payments — Checkout, Payment Intents, subscriptions — and to securely handle webhooks with signature verification.
license: MIT
---

# 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
```bash
npm install stripe @stripe/stripe-js
```

### 2. Configure env (Dashboard → Developers → API keys)
```bash
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...   # safe for the browser
STRIPE_SECRET_KEY=sk_test_...                    # SERVER ONLY — never expose
STRIPE_WEBHOOK_SECRET=whsec_...                  # from `stripe listen` or Dashboard
```

### 3. Server client
```ts
// lib/stripe.ts
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)
```ts
// app/api/checkout/route.ts
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", // "subscription" for recurring, "setup" to save a card
    line_items: [{ price: process.env.PRICE_ID!, quantity: 1 }], // never trust client-sent prices
    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)
```ts
// app/api/webhooks/stripe/route.ts
import { stripe } from "@/lib/stripe";

export async function POST(req: Request) {
  const body = await req.text();                    // RAW body — do not JSON.parse first
  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 });
  }

  // Return 2xx FAST; do heavy work async / idempotently
  switch (event.type) {
    case "checkout.session.completed":
      // fulfill the order — key off event.id to dedupe
      break;
    case "payment_intent.succeeded":
      break;
    default:
      break;
  }
  return new Response(null, { status: 200 });
}
```

### Recipe: Test webhooks locally
```bash
stripe login
stripe listen --forward-to localhost:3000/api/webhooks/stripe
# copy the printed whsec_... into STRIPE_WEBHOOK_SECRET
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
- [ ] Switch from `sk_test_`/`pk_test_` to **live** keys, set in the host secret store.
- [ ] Webhook endpoint is public **HTTPS** and registered in the Dashboard for the events you need.
- [ ] Signature verification uses the **raw** request body (disable body parsing / don't `JSON.parse` first).
- [ ] Handler returns `2xx` quickly; fulfillment runs async and is **idempotent** (dedupe by `event.id`).
- [ ] Don't assume event ordering; refetch objects via the API when needed.
- [ ] Exempt the webhook route from CSRF protection.
- [ ] Fulfill **only after** payment success events — never on the client redirect alone.

## 🔐 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
- https://docs.stripe.com/checkout/quickstart
- https://docs.stripe.com/payments/accept-a-payment
- https://docs.stripe.com/webhooks

More API Design skills

← All API Design skills

Check your AI visibility

One URL in, a 0–100 score and the exact fixes out.

RUN THE CHECK

Browse all the tools

15 tools across six categories
13 of them never send your data anywhere

Free · No signup · No trial clock

SEE THE DIRECTORY