Stripe Integration
Build or review a Stripe integration with current Stripe guidance: Checkout Sessions, Payment Element, Setup Intents, subscriptions and the Customer Portal, webhook-driven fulfillment, idempotency, restricted API keys and Stripe Tax. Use when: stripe, payments, checkout, subscription, billing, webhook, refund, payment intent.
npx claude-code-templates@latest --skill development/stripe-integration Scanned by NVIDIA SkillSpector
Automated static analysis (no LLM) · v2.12.0 · 2026-10-24
Partial analysis: some checks could not run on this component.
SkillSpector looks for patterns such as shell commands, credential access, network calls or unpinned dependencies. An observation is something worth reading in the files before you install, not a confirmed vulnerability. Components that run code locally, like hooks, MCP servers and security tooling, naturally show more of them.
- npx commands without a version suffix (e.g. @1.0.0) create a rug-pull risk if the upstream server is compromised and publishes a malicious update. ×3MCP Rug Pull · SKILL.md:206
Learn more about NVIDIA SkillSpector.
Content
Stripe Integration
Guidance for building payment code that does not lose orders, double-charge or
leak keys. It follows Stripe's own recommendations as of October 2026. When a
detail matters (a parameter, an event name, an API version), check
docs.stripe.com or the Stripe MCP server's
search_stripe_documentation tool before relying on memory.
Versions
- Latest API version:
2026-09-30.endive. Pin it explicitly on the client so an account default change never alters response shapes under you. - Latest server SDKs: Node.js 23, Python 16, Ruby 20, PHP 22, Java 34, Go 87,
.NET 53. Create a client instance; the global key pattern
(
stripe.api_key = …,Stripe.setApiKey) is deprecated in every SDK. - To move an existing integration to a newer version, read Upgrading your API version first.
// lib/stripe.ts
import Stripe from 'stripe';
export const stripe = new Stripe(process.env.STRIPE_API_KEY!, {
apiVersion: '2026-09-30.endive',
});Choose the API
| Building | Use |
|---|---|
| One-time payment, simple product | Payment Links (no code) or Checkout Sessions |
| One-time payment in your web app | Checkout Sessions (hosted or embedded) |
| Custom payment form | Checkout Sessions with ui_mode: 'custom' + Payment Element |
| Save a card for later | Setup Intents |
| Charge off-session (no customer present) | Payment Intents with a saved payment method |
| Subscriptions, seats, trials | Billing APIs + Checkout mode: 'subscription' + Customer Portal |
| New usage-based billing | Metronome (Stripe's usage platform), not raw Billing Meters |
| Marketplace or platform | Connect, Accounts v2 (/v2/core/accounts), Stripe-hosted onboarding |
Never use the Charges API, Sources API, Tokens API, Card Element or the
plan object. They are deprecated or legacy; use the rows above and Prices.
Rules
Fulfill from webhooks, never from the success page
A customer can pay and close the tab before the success page loads. Grant access, send email and write orders only in the webhook handler.
- Handle
checkout.session.completedandcheckout.session.async_payment_succeeded. Fulfill only whensession.payment_status !== 'unpaid'. Delayed methods (bank debits) sendcompletedwhile still unpaid. - Handle
checkout.session.async_payment_failedfor those failures. - Subscriptions need
customer.subscription.created|updated|deleted,invoice.paidandinvoice.payment_failed. Renewals, dunning and cancellations only reach you this way. - Webhooks are required before launch, not a follow-up.
Verify every webhook signature on the raw body
Signature verification needs the exact bytes Stripe sent. Body-parsing middleware breaks it.
// Next.js App Router: app/api/stripe/webhook/route.ts
import Stripe from 'stripe';
import { stripe } from '@/lib/stripe';
export async function POST(req: Request) {
const body = await req.text(); // raw body, do not use req.json()
const signature = req.headers.get('stripe-signature');
if (!signature) return new Response('missing signature', { status: 400 });
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(
body,
signature,
process.env.STRIPE_WEBHOOK_SECRET!,
);
} catch {
return new Response('invalid signature', { status: 400 });
}
switch (event.type) {
case 'checkout.session.completed':
case 'checkout.session.async_payment_succeeded': {
// Re-fetch instead of trusting the payload snapshot.
const session = await stripe.checkout.sessions.retrieve(event.data.object.id);
// fulfillOrder must be idempotent: the same event can arrive twice.
if (session.payment_status !== 'unpaid') await fulfillOrder(session.id);
break;
}
case 'checkout.session.async_payment_failed':
await markPaymentFailed(event.data.object.id);
break;
}
return new Response(null, { status: 200 });
}- On Cloudflare Workers, Deno or Bun use
await stripe.webhooks.constructEventAsync(...). - In Express, mount
express.raw({ type: 'application/json' })on the webhook route only, before any globalexpress.json(). - In live mode Stripe retries for up to three days and can deliver an event more than once
or out of order. Make
fulfillOrderidempotent (store processed event or session IDs with a unique constraint) and decide by the object's current state, not by event order. - Return 2xx quickly; move slow work to a queue.
Use idempotency keys on every create
A network retry without one can create a second charge or subscription.
await stripe.paymentIntents.create(
{ amount: 2000, currency: 'usd', customer: customerId },
{ idempotencyKey: `order-${orderId}-charge` },
);Derive the key from your own record (order ID, user ID + action), not from a random value per attempt, or retries get new keys.
Let Stripe pick payment methods
Do not pass payment_method_types (not even ['card']). Omitting it turns on
dynamic payment methods, configured in the Dashboard. To narrow the list use
payment_method_configuration, excluded_payment_method_types, or
allowed_payment_method_types on Payment and Setup Intents. The one exception
is Terminal: payment_method_types: ['card_present'].
Map Stripe objects to your records by ID
Store the Stripe Customer, Subscription and Checkout Session IDs on your own
rows, and resolve webhook events through those IDs. Use metadata (for
example client_reference_id or metadata.orderId on the session) as a
fallback, not as the primary link.
Keep subscription state in sync
Read access rights from your database, updated by the subscription webhooks above, and re-fetch the subscription from Stripe when a decision depends on it. Use the Customer Portal for upgrades, downgrades, cancellation and card updates instead of building those screens. Create one Product per plan and attach multiple Prices only for variants of the same plan (monthly, yearly, currency). Do not build renewal loops on raw Payment Intents; Billing handles retries and dunning (configure Smart Retries and emails in the Dashboard).
const session = await stripe.checkout.sessions.create({
mode: 'subscription',
customer: stripeCustomerId,
line_items: [{ price: priceId, quantity: 1 }],
subscription_data: { trial_period_days: 14 },
success_url: `${baseUrl}/billing/success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${baseUrl}/pricing`,
});Tax
automatic_tax: { enabled: true } collects nothing, and returns no error,
until the account has an active tax registration for the customer's
jurisdiction. Check registrations before telling anyone tax is on.
Keys and security
- Prefer restricted API keys (
rk_) with only the permissions each service needs over full secret keys (sk_). Use separate keys per environment. - Keep keys in the platform's secret store (Cloudflare and Vercel secrets,
AWS Secrets Manager, GCP Secret Manager, Azure Key Vault). Never in source,
client code, mobile apps, logs or error messages. Strings matching
[sr]k_live_in a repo are live keys: roll them immediately. - Treat the webhook signing secret (
whsec_) like an API key. - Add a pre-commit secret scan for
sk_andrk_strings. - Send a Content-Security-Policy that allows
https://*.stripe.cominscript-src,frame-srcandconnect-src. - In Connect OAuth flows, send and verify a random
statevalue.
Testing
- Develop against a sandbox, one per
developer or CI job, instead of the account's shared test mode.
stripe sandbox createmakes one without signing up. - Forward events locally:
stripe listen --forward-to localhost:3000/api/stripe/webhook(prints thewhsec_to use), thenstripe trigger checkout.session.completed. - Test cards:
4242 4242 4242 4242succeeds,4000 0025 0000 3155requires 3D Secure,4000 0000 0000 9995is declined (insufficient funds). - Use test clocks to move subscriptions through renewals and failed payments.
- Before launch, walk the go-live checklist and register the production webhook endpoint with only the events you handle.
Tools
- Stripe MCP server (
https://mcp.stripe.com, OAuth) gives the agent account data and doc search:npx claude-code-templates@latest --mcp devtools/stripe. - Ask before writes and money movement through the MCP:
npx claude-code-templates@latest --setting permissions/stripe-mcp-confirm-writes. - Forming the company: Stripe Atlas (Delaware C corp or LLC) can be started
from the MCP (
atlas_company_formation) or the CLI (stripe plugin install tools, thenstripe tools create_atlas_application). Atlas validates every value; identity, payment, signatures and submission happen in the Stripe Dashboard. Record the founders' decisions on entity type, equity and vesting; never choose them for the user. - Stripe publishes its own maintained agent skills (
npx skills add https://docs.stripe.comorstripe agent setup), includingstripe-best-practicesandupgrade-stripe.
Related Skills
Works well with: nextjs-supabase-auth, nextjs-app-router-patterns, supabase-postgres-best-practices, api-security-best-practices