Claude Code Stripe Integration Guide
Claude Code and Stripe
Stripe is the payments layer for most SaaS products. It is also one of the more complex third-party integrations to get right: payment intents, subscription lifecycle management, webhook reliability, idempotency, and proration logic all have sharp edges.
Claude Code handles Stripe integration work well because it can hold your entire payment flow in context, knows the Stripe SDK patterns, and generates production-grade code that handles failure cases. Not tutorial code that skips error handling and webhook verification.
This guide covers: Stripe MCP setup, payment intents, subscriptions, webhook handlers with signature verification, idempotency, and the CLAUDE.md configuration that makes every Stripe session start from the right baseline.
Environment setup
Before any Stripe work, structure your credentials correctly. Claude Code looks for these patterns:
# .env.local (server-side only, never commit)
STRIPE_SECRET_KEY=sk_live_...
STRIPE_WEBHOOK_SECRET=whsec_...
# .env (safe to commit)
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_live_...
The separation matters: the secret key must never reach the browser. Claude Code understands this distinction and will refuse to use STRIPE_SECRET_KEY in client-side code if you document the convention in your CLAUDE.md. See the full environment variables guide for how to structure credentials across environments.
Add a Stripe section to your CLAUDE.md:
## Payments (Stripe)
### Credentials
- Secret key: STRIPE_SECRET_KEY (server only, never import in client components)
- Publishable key: NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY (safe for browser)
- Webhook secret: STRIPE_WEBHOOK_SECRET (for verifying incoming webhooks)
### API version
- Current: 2026-04-22.dahlia
- Always pin the API version on the Stripe client init
### Patterns
- Use idempotency keys on all create operations (payment intents, customers, subscriptions)
- Idempotency key format: `{operation}-{userId}-{timestamp-day}` (day-scoped so retries in the same day reuse the key)
- Webhook handler: /api/webhooks/stripe, raw body required (do NOT parse as JSON before verification)
- Currency: GBP, always specify in pence (not pounds)
- Always create a Stripe customer on user sign-up and store customer_id in your database
### Stripe client init
Initialize once and export:
- Server: import from '@/lib/stripe' (uses STRIPE_SECRET_KEY)
- Never initialize Stripe with the secret key in a route handler inline, always import the shared instance
With this in place, Claude Code applies these conventions to every Stripe file it touches. You do not need to repeat them.
Setting up the Stripe MCP server
For development-time access, the Stripe MCP server lets Claude Code query your Stripe account directly: customers, payments, subscriptions, and products.
{
"mcpServers": {
"stripe": {
"command": "npx",
"args": ["-y", "@stripe/mcp-server-stripe"],
"env": {
"STRIPE_SECRET_KEY": "sk_test_..."
}
}
}
}
Use your test key here. The MCP server gives Claude live read access during development: "List all active subscriptions", "What is the current price for the pro plan?", "Show me failed payments from the last week" all become direct queries rather than context switches to the Stripe dashboard.
For a broader overview of MCP server patterns, see the Claude Code MCP servers guide.
Payment intents
A payment intent represents a single charge attempt. Claude Code generates the full payment intent flow: server-side creation, client-side confirmation, and success/failure handling.
Prompt:
Build a payment intent flow for one-time purchases.
The user selects a product, we create a payment intent server-side,
and confirm it in the browser with Stripe Elements.
Handle payment failures, card authentication, and success redirect.
TypeScript, Next.js App Router.
Server action for payment intent creation:
// src/app/actions/create-payment-intent.ts
'use server'
import { stripe } from '@/lib/stripe'
import { auth } from '@/lib/auth'
import { db } from '@/lib/db'
export async function createPaymentIntent(productId: string) {
const session = await auth()
if (!session?.user?.id) throw new Error('Unauthorized')
const product = await db.product.findUnique({ where: { id: productId } })
if (!product) throw new Error('Product not found')
const customer = await db.user.findUnique({
where: { id: session.user.id },
select: { stripeCustomerId: true },
})
const paymentIntent = await stripe.paymentIntents.create(
{
amount: product.priceInPence,
currency: 'gbp',
customer: customer?.stripeCustomerId ?? undefined,
metadata: {
userId: session.user.id,
productId: product.id,
},
automatic_payment_methods: { enabled: true },
},
{
idempotencyKey: `payment-intent-${session.user.id}-${productId}-${new Date().toISOString().slice(0, 10)}`,
}
)
return { clientSecret: paymentIntent.client_secret }
}
Notice the idempotency key. Claude Code includes it by default when your CLAUDE.md documents the convention. Without it, network retries create duplicate charges. With it, retries within the same day safely reuse the existing payment intent.
Subscriptions
Subscription logic is more complex than one-time payments because it spans multiple lifecycle events: trial start, trial end, payment success, payment failure, renewal, upgrade, downgrade, and cancellation. Claude Code manages this complexity well when you describe the full subscription model upfront.
Build a subscription system with three tiers: free, pro (£12/mo), enterprise (£49/mo).
Stripe handles billing. Our database tracks the current tier.
Implement:
- Stripe Checkout session creation for upgrades
- Customer portal for self-service management
- Proration on mid-cycle plan changes
- Grace period of 3 days on payment failure before downgrading to free
Claude Code generates the checkout session handler, portal session handler, and the webhook event handlers for the full subscription lifecycle. The key webhook events for subscriptions are:
customer.subscription.created, new subscription, update database tiercustomer.subscription.updated, plan change or status change, sync databasecustomer.subscription.deleted, cancellation, downgrade to freeinvoice.payment_succeeded, renewal confirmed, extend subscription periodinvoice.payment_failed, payment failed, start grace period logic
Claude Code generates handlers for all of these in one pass.
Webhook handlers with signature verification
Webhooks are where Stripe integrations fail most often in production. The three most common problems: not verifying the signature (security hole), parsing the body as JSON before verification (breaks the signature check), and not handling idempotency (processing the same event twice).
Claude Code generates webhook handlers that solve all three:
Write a Stripe webhook handler for Next.js App Router.
Handle: checkout.session.completed, customer.subscription.updated,
customer.subscription.deleted, invoice.payment_succeeded, invoice.payment_failed.
Verify the Stripe signature. Use raw body, do not parse as JSON before verification.
Store each processed event ID to prevent double-processing.
Generated handler:
// src/app/api/webhooks/stripe/route.ts
import { NextRequest, NextResponse } from 'next/server'
import Stripe from 'stripe'
import { stripe } from '@/lib/stripe'
import { db } from '@/lib/db'
import { handleCheckoutCompleted } from './handlers/checkout-completed'
import { handleSubscriptionUpdated } from './handlers/subscription-updated'
import { handleSubscriptionDeleted } from './handlers/subscription-deleted'
import { handleInvoicePaymentSucceeded } from './handlers/invoice-payment-succeeded'
import { handleInvoicePaymentFailed } from './handlers/invoice-payment-failed'
export const config = { api: { bodyParser: false } }
export async function POST(req: NextRequest) {
const body = await req.text()
const signature = req.headers.get('stripe-signature')
if (!signature) {
return NextResponse.json({ error: 'Missing signature' }, { status: 400 })
}
let event: Stripe.Event
try {
event = stripe.webhooks.constructEvent(
body,
signature,
process.env.STRIPE_WEBHOOK_SECRET!
)
} catch (err) {
console.error('Webhook signature verification failed:', err)
return NextResponse.json({ error: 'Invalid signature' }, { status: 400 })
}
// Idempotency: skip if already processed
const existingEvent = await db.stripeEvent.findUnique({
where: { stripeEventId: event.id },
})
if (existingEvent) {
return NextResponse.json({ received: true, duplicate: true })
}
// Record the event before processing
await db.stripeEvent.create({
data: {
stripeEventId: event.id,
type: event.type,
processedAt: new Date(),
},
})
try {
switch (event.type) {
case 'checkout.session.completed':
await handleCheckoutCompleted(event.data.object as Stripe.Checkout.Session)
break
case 'customer.subscription.updated':
await handleSubscriptionUpdated(event.data.object as Stripe.Subscription)
break
case 'customer.subscription.deleted':
await handleSubscriptionDeleted(event.data.object as Stripe.Subscription)
break
case 'invoice.payment_succeeded':
await handleInvoicePaymentSucceeded(event.data.object as Stripe.Invoice)
break
case 'invoice.payment_failed':
await handleInvoicePaymentFailed(event.data.object as Stripe.Invoice)
break
default:
console.log(`Unhandled event type: ${event.type}`)
}
} catch (err) {
console.error(`Error processing event ${event.id}:`, err)
// Return 500 so Stripe retries, idempotency key ensures safe retry
return NextResponse.json(
{ error: 'Processing failed' },
{ status: 500 }
)
}
return NextResponse.json({ received: true })
}
Three things in this handler that tutorial code usually skips:
Raw body. The webhook uses
req.text(), notreq.json(). Stripe's signature verification computes an HMAC over the raw bytes. JSON parsing normalizes whitespace and can change the byte sequence, which breaks the signature check.Event deduplication. The
stripeEventtable records every processed event ID. If Stripe retries a delivery (which it does on 5xx responses), the handler returns 200 without reprocessing. Without this, a single failed database write can result in duplicate subscription updates.Return 500 on processing failure. Returning 500 tells Stripe to retry. The idempotency check at the top means retries are safe. Return 200 and your events silently fail with no retry.
Using hooks for payment safety
Claude Code's hook system adds a useful safety layer for Stripe work. A PreToolUse hook can warn before Claude writes to production Stripe resources:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "bash -c 'echo \"$CLAUDE_TOOL_INPUT\" | grep -i \"sk_live\" && echo \"WARNING: About to use live Stripe key. Confirm this is intentional.\" && exit 1 || exit 0'"
}
]
}
]
}
}
This blocks any Bash command containing the live key string, forcing explicit confirmation before Claude runs anything against production Stripe. For payment systems, being conservative about what runs automatically is the right default.
For a complete treatment of Claude Code hooks and permission controls, see the Claude Code hooks guide.
Testing Stripe integrations
Testing payment flows requires mocking the Stripe SDK correctly. Claude Code generates tests that cover happy paths, declined cards, and webhook delivery. The key: mock at the HTTP layer, not the Stripe client methods, so your tests exercise the actual error handling code.
Prompt:
Write tests for the payment intent flow.
Cover: successful payment, card declined, insufficient funds,
authentication required (3DS), network error.
Mock the Stripe HTTP layer, not the SDK methods.
Also write a webhook handler test with a real Stripe test event payload.
For the full testing strategy, see our Claude Code testing guide. The short version: use Stripe's test card numbers in integration tests (4000000000000002 for declined, 4000002760003184 for 3DS required) and test the webhook handler by constructing a real Stripe event object with stripe.webhooks.generateTestHeaderString().
Debugging Stripe issues
When something breaks in a Stripe integration, Claude Code diagnoses faster than searching the Stripe docs because it can read your actual code and error logs together.
Common scenarios:
Webhook not receiving events. Claude Code checks: is the endpoint URL correct in the Stripe dashboard? Is the webhook secret matching what is in .env? Is the handler returning the right status codes? Is the server accessible (localhost webhooks need Stripe CLI forwarding with stripe listen --forward-to localhost:3000/api/webhooks/stripe)?
Idempotency key collisions. If two different operations share a key, Stripe returns the first operation's result for the second. Claude Code audits your key generation logic and flags any cases where different payment intents could generate the same key.
Subscription status out of sync. If your database subscription tier does not match the Stripe subscription status, Claude Code reconciles them: it queries the Stripe MCP server for the current subscription state and generates a script to sync your database.
Proration surprises. Proration math on mid-cycle upgrades is counterintuitive. Claude Code explains the proration model for any specific scenario and generates the preview API call so users can see the charge before confirming: stripe.invoices.retrieveUpcoming().
Full workflow: adding a paid plan
Here is how a complete Stripe feature session looks with Claude Code properly configured:
Define the pricing. "Add a Pro plan at £12/month. Users on Free get unlimited read access and 3 exports per month. Users on Pro get unlimited exports and priority support."
Claude Code creates the Stripe products. Via the MCP server, it creates the product and price in your Stripe account (test mode), then logs the price IDs for your CLAUDE.md.
Claude Code scaffolds the checkout flow. Checkout session creation, success redirect, cancel redirect.
Claude Code writes the webhook handlers. For
checkout.session.completedto provision Pro access, andcustomer.subscription.deletedto revert to Free.Claude Code updates the database schema. Adds
subscription_tier,stripe_customer_id, andstripe_subscription_idto the users table, generates the migration.Claude Code writes the access gates. Middleware or server action guards that check the subscription tier before allowing export operations.
Claude Code writes the tests. Mocked Stripe responses for happy path and cancellation.
The session takes less than an hour for a complete paid plan feature. The Claude Code best practices guide covers how to structure these multi-step sessions so Claude Code stays focused and does not drift mid-way through.
Getting to production
When moving from Stripe test mode to live mode:
- Swap
sk_test_forsk_live_in your production environment (never in.env.localwhich gets committed) - Register your production webhook endpoint in the Stripe dashboard and get the live
whsec_secret - Run
stripe listen --forward-toonly in development, never production - Enable Stripe Radar for fraud detection on live transactions
- Set up Stripe's automatic email receipts or build your own via webhooks
Claude Code handles the code for all of these. The Stripe dashboard configuration is the one part it cannot do directly (unless you use the Stripe MCP server with a live key, which you should do deliberately and with the hook guardrails in place).
For deploying your application with Stripe configured, make sure the live credentials are set in your production environment variables before the first deploy.
FAQ
How does Claude Code avoid hardcoding Stripe keys?
When your CLAUDE.md documents the credential pattern, Claude Code always references process.env.STRIPE_SECRET_KEY rather than the key value. It also avoids reading your .env files and outputting their contents. For additional enforcement, the hook example above blocks any command containing a live key string.
Can Claude Code create Stripe products and prices for me?
Yes, via the Stripe MCP server. Ask "Create a Pro plan product with a monthly price of £12 and a yearly price of £100" and Claude Code executes the API calls directly. It returns the price IDs so you can store them in your CLAUDE.md for reference.
How do I test webhooks locally?
Use the Stripe CLI: stripe listen --forward-to localhost:3000/api/webhooks/stripe. This proxies Stripe's webhook delivery to your local server. Claude Code knows this command and will include it in setup instructions when you ask about local webhook testing.
What is the right way to handle Stripe errors in production?
Stripe errors fall into two categories: errors you surface to the user (card declined, insufficient funds, card expired) and errors you log and investigate (API errors, network timeouts, unexpected responses). Claude Code generates error handling that distinguishes between them: user-facing errors get clear messages (do not expose raw Stripe error codes), internal errors get structured logging with the Stripe request ID for debugging.
Get Claudify, pre-built Stripe webhook handlers, subscription commands, and payment flow templates. Installed in one command: npx create-claudify.
More like this
Ready to upgrade your Claude Code setup?
Get Claudify