Rule Writing

CLAUDE.md for Stripe Integration

AI generates Stripe code that handles money client-side and skips webhook verification. Rules for server-side checkout, webhooks, idempotency, and PCI compliance.

8 min read·August 4, 2025

AI creates Stripe charges client-side — the user controls the amount

Server-side checkout, webhook verification, idempotency keys, and PCI compliance

Why Stripe Integration Needs Security-First Rules

Payment processing is the highest-stakes code AI generates. A Stripe integration bug doesn't cause a bad user experience — it causes lost money, double charges, or security vulnerabilities that expose payment data. AI assistants generate Stripe code that: creates charges client-side (users can modify amounts), skips webhook signature verification (attackers can fake events), doesn't use idempotency keys (network retries create double charges), and stores card numbers (PCI violation).

Stripe's API is designed for server-side use — the client collects payment details through Stripe Elements or Checkout, and the server creates the charge. AI that puts charge creation on the client is a fundamental security violation: the user controls the request, including the amount.

These rules target Stripe's latest API (2024+) with TypeScript. They cover Checkout Sessions, webhooks, subscriptions, and error handling patterns.

Rule 1: Server-Side Checkout Sessions — Never Client-Side Charges

The rule: 'Create Checkout Sessions on the server: const session = await stripe.checkout.sessions.create({ line_items: [{ price: priceId, quantity: 1 }], mode: "payment", success_url: "https://example.com/success", cancel_url: "https://example.com/cancel" }). Return the session URL to the client: redirect to session.url. The client never sees the amount, currency, or any payment parameter — the server defines everything.'

For custom flows: 'Use Payment Intents for custom checkout UIs: create the PaymentIntent server-side (stripe.paymentIntents.create({ amount: 2000, currency: "usd" })), pass the client_secret to the frontend, and confirm with Stripe.js (stripe.confirmCardPayment(clientSecret)). The amount is set server-side — the client can't modify it.'

AI generates stripe.charges.create on the client — or worse, passes the amount from the client to the server without validation. One rule: 'all Stripe API calls happen server-side, amounts come from your database/config, never from the client request.'

  • Checkout Sessions: create server-side, redirect client to session.url
  • Payment Intents: create server-side, pass client_secret, confirm with Stripe.js
  • Never create charges from client-side code — user controls the request
  • Amounts from database/config — never from req.body or client input
  • stripe secret key on server only — publishable key on client only
⚠️ Never Client-Side Charges

AI puts stripe.charges.create on the client — the user controls the request, including the amount. Server-side only: create Checkout Session or Payment Intent on the server, amounts from your database, never from req.body.

Rule 2: Webhook Signature Verification

The rule: 'Verify every webhook signature: const event = stripe.webhooks.constructEvent(req.body, sig, endpointSecret). Use the raw request body (not parsed JSON) for verification. If verification fails, return 400 — the event is forged. Never process webhook events without signature verification — attackers can send fake events to trigger actions (free subscriptions, fake payments).'

For webhook handlers: 'Handle events idempotently — webhooks can be delivered multiple times. Use the event.id to check if you've already processed this event. Store processed event IDs in your database. Return 200 quickly — do heavy processing in a background job, not in the webhook handler (Stripe times out after 20 seconds).'

For event types: 'Handle: checkout.session.completed (payment succeeded), invoice.paid (subscription renewed), invoice.payment_failed (subscription payment failed), customer.subscription.deleted (subscription cancelled). Don't rely solely on success_url redirect — the user might close the browser before redirecting. Webhooks are the source of truth for payment status.'

ℹ️ Webhooks = Source of Truth

Don't rely on success_url redirect for payment confirmation — the user might close the browser. Webhooks (checkout.session.completed) are the reliable confirmation. Verify signature, process idempotently, return 200 fast.

Rule 3: Idempotency Keys for Every Mutation

The rule: 'Use idempotency keys on every Stripe API call that creates or modifies resources: stripe.charges.create({ ... }, { idempotencyKey: orderId }). The idempotency key ensures that if the same request is sent twice (network retry, user double-click), Stripe returns the same result — no double charge. Use your order/transaction ID as the idempotency key — it's naturally unique per operation.'

For the pattern: 'Create order in your database (status: pending) → Create Stripe charge with order ID as idempotency key → On success: update order status to paid → On webhook confirmation: finalize the order. If the charge request fails mid-flight and retries, the idempotency key ensures Stripe doesn't create a duplicate charge.'

AI never includes idempotency keys — every retry creates a new charge. For a user who clicks 'Pay' and gets a timeout, the retry creates a second charge. One idempotency key per operation prevents every double-charge scenario.

  • Idempotency key on every create/update: { idempotencyKey: orderId }
  • Use order/transaction ID — naturally unique per operation
  • Retries with same key = same result — no duplicate charges
  • Create order first (pending) → charge → update order (paid) → webhook confirms
  • Without idempotency: timeout + retry = double charge. With: safe retry.
💡 One Key, No Double Charge

idempotencyKey: orderId on every Stripe create/update. Timeout → retry with same key → Stripe returns the original result. Without it: timeout → retry → second charge. One parameter prevents every double-charge scenario.

Rule 4: Stripe Error Handling

The rule: 'Catch Stripe errors by type: try { await stripe.charges.create(...) } catch (err) { if (err.type === "StripeCardError") { /* card declined — show user message */ } else if (err.type === "StripeInvalidRequestError") { /* bug in your code — log and alert */ } else { /* unexpected — log, return 500 */ } }. Never show raw Stripe error messages to users — they may contain technical details. Map error types to user-friendly messages.'

For decline handling: 'Card declines (StripeCardError) have decline codes: err.code === "insufficient_funds", "expired_card", "incorrect_cvc". Map to user messages: "Your card was declined. Please try a different payment method." Never reveal the specific decline reason from the issuer — it can be used for card testing fraud.'

For logging: 'Log every Stripe API call with: request ID (err.requestId), error type, error code, and your internal order/customer ID. Never log full card numbers or CVCs — they violate PCI requirements. Log the last 4 digits at most. Use Stripe's Dashboard for detailed request logs.'

Rule 5: Subscription and Billing Patterns

The rule: 'Use Stripe Billing for subscriptions — never build custom recurring billing logic. Create subscriptions server-side: stripe.subscriptions.create({ customer: customerId, items: [{ price: priceId }] }). Handle lifecycle through webhooks: invoice.paid (renewal), invoice.payment_failed (retry), customer.subscription.updated (plan change), customer.subscription.deleted (cancellation). Sync subscription status to your database via webhooks — never trust client-side state.'

For the customer portal: 'Use Stripe's hosted Customer Portal for: plan changes, payment method updates, cancellations, and invoice history. Redirect: const session = await stripe.billingPortal.sessions.create({ customer: customerId }). This eliminates building subscription management UI — Stripe handles it with their hosted, PCI-compliant interface.'

For metered billing: 'Use usage records for metered subscriptions: stripe.subscriptionItems.createUsageRecord(subItemId, { quantity: 100, timestamp: now }). Report usage server-side at regular intervals. Stripe calculates the invoice based on accumulated usage. Never let the client report usage — it can be manipulated.'

Complete Stripe Rules Template

Consolidated rules for Stripe payment integration.

  • Server-side only: Checkout Sessions, Payment Intents — never client-side charges
  • Amounts from DB/config — never from client input — secret key server-only
  • Webhook signature verification: constructEvent — 400 on invalid signature
  • Handle webhooks idempotently: check event.id, return 200 fast, process in background
  • Idempotency key on every mutation: order ID as key — safe retries, no double charges
  • Error handling by type: StripeCardError for users, StripeInvalidRequestError for bugs
  • Stripe Billing for subscriptions — Customer Portal for self-service — webhooks for lifecycle
  • Never log full card numbers — PCI compliance — Stripe Dashboard for request logs