Build This Now
Build This Now
O que é o Código Claude?Instalar o Claude CodeInstalador Nativo do Claude CodeO Teu Primeiro Projeto com Claude Code
Claude Code v2.1.122 Release NotesMelhores Práticas do Claude CodeBoas Práticas para o Claude Opus 4.7Claude Code num VPSIntegração GitRevisão de Código com ClaudeWorktrees no Claude CodeControle Remoto do Claude CodeChannels do Claude CodeTarefas Agendadas no Claude CodePermissões do Claude CodeModo Auto do Claude CodeAdding Stripe Payments With Claude CodeFeedback LoopsFluxos de Trabalho com TodosTarefas no Claude CodeTemplates de ProjetoPreços e Consumo de Tokens no Claude CodeClaude Code Pricing: What You'll Actually PayClaude Code Ultra ReviewBuilding a Next.js App With Claude CodeClaude Code With Supabase: Database, Auth, RLS
speedy_devvkoen_salo
Blog/Handbook/Workflow/Adding Stripe Payments With Claude Code

Adding Stripe Payments With Claude Code

Wire up Stripe Checkout, webhooks, and the customer portal in a Next.js app using Claude Code. From first prompt to live payment in one session.

Pare de configurar. Comece a construir.

Templates SaaS com orquestração de IA.

Published May 3, 20269 min readHandbook hubWorkflow index

Problem: Stripe integration has 7 moving parts: checkout, webhooks, the customer portal, environment variables, test keys, live keys, and the CLI. Most tutorials cover three of them and leave you debugging the rest at midnight.

Quick Win: Add the Stripe MCP server to Claude Code in one command so Claude can look up customers, prices, and subscriptions in real time while you build:

claude mcp add --transport http stripe https://mcp.stripe.com/

That single line gives Claude direct access to your Stripe account. It can read live data, verify your products exist, and catch config errors before they hit your webhook handler.

What You Need Before Starting

Three things need to be in place.

A Stripe account with at least one product and price created. You can have Claude create them via the MCP server without touching the Dashboard. Once the MCP is connected, a prompt like "create a product called Pro Plan with a $29/month recurring price" is enough.

Claude Code with the Stripe MCP connected (command above). The alternative is the Stripe best-practices skill, which injects Stripe documentation into your session:

npx skills add https://docs.stripe.com --yes

The MCP server is the better choice for active development. Claude can call it mid-session to list your prices, look up a customer by email, retrieve a subscription's current status, or verify that your webhook secret matches what the Dashboard shows. The skill gives you documentation context only. It does not query your account.

The two are not mutually exclusive. Some teams run both: MCP for live queries, skill for best-practice guidance. The MCP with a restricted API key (rk_test_...) is the safer setup in test mode because restricted keys limit which Stripe objects Claude can touch.

Four environment variables in your .env.local file:

STRIPE_SECRET_KEY=sk_test_...
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
NEXT_PUBLIC_URL=http://localhost:3000

STRIPE_SECRET_KEY never touches the client. NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY is safe to expose. STRIPE_WEBHOOK_SECRET comes from the Stripe CLI when you start local event forwarding (covered in the testing section). NEXT_PUBLIC_URL feeds your success_url and return_url parameters.

Checkout Sessions vs. Payment Intents

Two Stripe APIs can take a payment. Picking the wrong one adds weeks of work.

Checkout Sessions (stripe.checkout.sessions.create()) redirect the user to a Stripe-hosted page or an embedded form on your domain. Stripe handles card input, 3D Secure, Apple Pay, currency display, and tax. For most SaaS products, this is the right choice. One-time purchases and subscriptions both run through it.

Payment Intents (stripe.paymentIntents.create()) are lower-level. You build the form yourself with Stripe Elements, collect card data, and call the API. More control, more code, more maintenance. Use this only when you need a fully custom checkout UI that a hosted page cannot deliver.

The 2026 preferred pattern for Checkout Sessions is Embedded Checkout: ui_mode: 'embedded' keeps the user on your domain, uses EmbeddedCheckoutProvider and EmbeddedCheckout components from @stripe/react-stripe-js, and returns to a return_url you set. Hosted Checkout (redirect to stripe.com) still works, but Embedded is what Stripe steers new integrations toward.

Building the Checkout Flow

Ask Claude to scaffold the route, then verify the mode param before shipping.

A useful prompt to give Claude here is: "Build me a Stripe Checkout Sessions route in app/api/checkout/route.ts. Use mode: payment for one-time purchases, return the session URL as JSON, and read all URLs from environment variables."

For a one-time purchase, the route looks like this:

// app/api/checkout/route.ts
import { NextResponse } from 'next/server'
import Stripe from 'stripe'

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
  apiVersion: '2024-11-20.acacia',
})

export async function POST(request: Request) {
  const { priceId } = await request.json()

  const session = await stripe.checkout.sessions.create({
    mode: 'payment',
    line_items: [{ price: priceId, quantity: 1 }],
    success_url: `${process.env.NEXT_PUBLIC_URL}/success?session_id={CHECKOUT_SESSION_ID}`,
    cancel_url: `${process.env.NEXT_PUBLIC_URL}/pricing`,
  })

  return NextResponse.json({ url: session.url })
}

For a subscription, change exactly one thing. Set mode: 'subscription' instead of mode: 'payment'. Your priceId must point to a recurring price (not a one-time price) in Stripe. Everything else in the route stays identical.

To switch to Embedded Checkout, add ui_mode: 'embedded' and replace success_url with return_url. The response returns session.client_secret instead of session.url, which the EmbeddedCheckoutProvider uses on the frontend.

Webhooks: The Part Everyone Gets Wrong

Webhooks are where Stripe talks back to your app. Every provisioning decision (grant access, revoke access, send a receipt) should live in the webhook handler, not in the checkout success redirect. Users close tabs and hit the back button. The redirect is unreliable. The webhook always fires.

The critical issue in Next.js App Router is raw body parsing. Stripe's signature verification (stripe.webhooks.constructEvent) requires the raw request body as a string. App Router parses the body before your handler sees it. If you call request.json(), the stream is consumed and verification fails every time.

Give Claude the exact constraint: "Build a Stripe webhook handler at app/api/stripe-webhook/route.ts. It must use request.text() for the raw body, not request.json(), and verify the signature before processing any events."

Use request.text() instead:

// app/api/stripe-webhook/route.ts
import { NextResponse } from 'next/server'
import Stripe from 'stripe'

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
  apiVersion: '2024-11-20.acacia',
})

export async function POST(request: Request) {
  const rawBody = await request.text()
  const sig = request.headers.get('stripe-signature')!

  let event: Stripe.Event

  try {
    event = stripe.webhooks.constructEvent(
      rawBody,
      sig,
      process.env.STRIPE_WEBHOOK_SECRET!
    )
  } catch (err) {
    return NextResponse.json(
      { error: 'Webhook signature verification failed' },
      { status: 400 }
    )
  }

  switch (event.type) {
    case 'checkout.session.completed': {
      const session = event.data.object as Stripe.Checkout.Session
      // Provision access, send welcome email
      await grantAccess(session.customer as string)
      break
    }
    case 'customer.subscription.updated': {
      const sub = event.data.object as Stripe.Subscription
      // Update tier in DB
      await updateSubscription(sub.customer as string, sub.status)
      break
    }
    case 'customer.subscription.deleted': {
      const sub = event.data.object as Stripe.Subscription
      // Revoke access
      await revokeAccess(sub.customer as string)
      break
    }
    case 'invoice.payment_failed': {
      const invoice = event.data.object as Stripe.Invoice
      // Send dunning email, flag in DB
      await handleFailedPayment(invoice.customer as string)
      break
    }
  }

  return NextResponse.json({ received: true })
}

The four events to handle at minimum are in the table below. The rest can be added as your billing logic grows.

EventWhen it firesAction
checkout.session.completedPayment or subscription startedProvision access, send welcome email
customer.subscription.updatedPlan change, renewal, trial endUpdate DB tier
customer.subscription.deletedCancellationRevoke access
invoice.payment_failedRecurring charge failedSend dunning email, flag in DB

One more thing: this route must be excluded from any Next.js middleware that reads the body, and it must not be wrapped in your app's auth checks. Stripe posts to it as an unauthenticated request. Protect it with signature verification only.

Customer Portal Setup

The customer portal lets users manage their own subscription: cancel, update payment info, view invoices. One step trips up most developers.

You must configure the portal in the Stripe Dashboard before creating any portal sessions. Without a saved configuration, every billingPortal.sessions.create() call returns an error. The configuration URL for test mode is https://dashboard.stripe.com/test/settings/billing/portal. Live mode has its own separate URL at https://dashboard.stripe.com/settings/billing/portal. Both require configuration before you deploy.

The configuration step is where you decide what actions users are allowed to take: cancel their subscription, update their payment method, switch plans, download invoices. Set those options in the Dashboard, save the configuration, and then your API calls will work.

Once configured, the route to create a session is short:

// app/api/portal/route.ts
import { NextResponse } from 'next/server'
import Stripe from 'stripe'

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
  apiVersion: '2024-11-20.acacia',
})

export async function POST(request: Request) {
  const { customerId } = await request.json()

  const session = await stripe.billingPortal.sessions.create({
    customer: customerId,
    return_url: `${process.env.NEXT_PUBLIC_URL}/account`,
  })

  return NextResponse.json({ url: session.url })
}

Your frontend redirects to session.url. The user sees their Stripe billing page, makes changes, and returns to /account when done. The changes fire as customer.subscription.updated or customer.subscription.deleted events, which your webhook handler already covers.

Note: test mode and live mode portal configurations are completely separate. Configuring the portal in test mode does not set it up for live mode. Go through the same configuration steps in both environments before going live.

Testing the Full Flow

The Stripe CLI forwards live events from Stripe to your local server. Run this in a separate terminal before testing any payments:

stripe listen --forward-to localhost:3000/api/stripe-webhook

The CLI prints a webhook signing secret when it starts. That value goes in STRIPE_WEBHOOK_SECRET in your .env.local. It changes every time you restart the listener, so don't hardcode it.

To fire a specific event without clicking through a payment form, use stripe trigger:

stripe trigger checkout.session.completed
stripe trigger customer.subscription.deleted
stripe trigger invoice.payment_failed

Test cards for different scenarios:

ScenarioCard number
Successful payment4242 4242 4242 4242
Declined4000 0000 0000 0002
3D Secure required4000 0025 0000 3155
Insufficient funds4000 0000 0000 9995

Use any future expiry date, any 3-digit CVC, and any ZIP code. The card number is the only value that changes the outcome.

The recommended test sequence is: start the CLI listener, open your checkout page, complete a payment with card 4242 4242 4242 4242, watch the checkout.session.completed event arrive in your terminal, and verify your handler ran (check the DB or logs). Then trigger stripe trigger invoice.payment_failed to confirm your dunning path works without waiting for a real failed charge. Cover the 3D Secure card (4000 0025 0000 3155) last. Stripe redirects through an authentication step that your frontend must handle. If the redirect fails silently, the session never completes and no webhook fires.

Going Live Checklist

Switching from test to live mode is four steps, not one key swap.

First, replace your test keys (sk_test_, pk_test_) with live keys (sk_live_, pk_live_) in your production environment variables.

Second, configure the customer portal in live mode at https://dashboard.stripe.com/settings/billing/portal. Test mode and live mode have completely separate portal configurations. What you set in test mode does not carry over.

Third, set your webhook endpoint in the Stripe Dashboard to your production URL. Go to Developers > Webhooks > Add endpoint and point it at https://yourdomain.com/api/stripe-webhook. Copy the live webhook signing secret and set it as STRIPE_WEBHOOK_SECRET in production.

Fourth, confirm your price IDs. Test mode prices (price_test_...) do not exist in live mode. Any hardcoded price IDs need to be updated to live price IDs or read from environment variables.

Run one real payment with a real card (then refund it) before calling the integration done. The CLI trigger is good for testing handlers, but nothing confirms the full path like an actual charge.

What Build This Now Ships Pre-Built

Writing and debugging this flow takes a full day for most developers. Stripe's checkout edge function, the webhook handler, the customer portal route, and all four environment variables are pre-wired in Build This Now from day one. The integration deploys to Supabase Edge Functions, tested, and working before you write your first custom feature.

Stripe is not the hard part. Getting every piece connected in the right order is. Once you've done it once, you know where the gaps are.

Continue in Workflow

  • Melhores Práticas do Claude Code
    Cinco hábitos separam os engenheiros que entregam com Claude Code: PRDs, regras modulares em CLAUDE.md, slash commands personalizados, resets com /clear e uma mentalidade de evolução do sistema.
  • Modo Auto do Claude Code
    Um segundo modelo Sonnet revê cada chamada de ferramenta do Claude Code antes de ser executada. O que o modo auto bloqueia, o que permite e as regras de permissão que cria nas tuas definições.
  • Channels do Claude Code
    Liga o Claude Code ao Telegram, Discord ou iMessage com plugins MCP. Walkthroughs de configuração e os fluxos de trabalho assíncronos e mobile-first que tornam a ligação válida.
  • Building a Next.js App With Claude Code
    How to use Claude Code to build a full Next.js 16 app — from project setup through App Router, Server Components, and deployment.
  • Claude Code Pricing: What You'll Actually Pay
    Claude Code is free to install. What you pay depends on your plan. A plain-English breakdown of every tier, real usage costs, and which plan fits your workflow.
  • Claude Code With Supabase: Database, Auth, RLS
    Set up Supabase in a Next.js project using Claude Code: migrations, row-level security policies, auth, and edge functions from a single terminal.

More from Handbook

  • Fundamentos do agente
    Cinco maneiras de criar agentes especializados no Código Claude: Sub-agentes de tarefas, .claude/agents YAML, comandos de barra personalizados, personas CLAUDE.md e prompts de perspetiva.
  • Engenharia de Harness para Agentes
    O harness é cada camada ao redor do seu agente de IA, exceto o modelo em si. Aprenda os cinco pontos de controle, o paradoxo das restrições, e por que o design do harness determina o desempenho do agente mais do que o modelo.
  • Padrões de Agentes
    Orchestrator, fan-out, cadeia de validação, routing especializado, refinamento progressivo e watchdog. Seis formas de orquestração para ligar sub-agentes no Claude Code.
  • Boas Práticas para Equipas de Agentes
    Padrões testados em produção para Equipas de Agentes Claude Code. Prompts de criação ricos em contexto, tarefas bem dimensionadas, posse de ficheiros, modo delegado, e correções das versões v2.1.33-v2.1.45.

Pare de configurar. Comece a construir.

Templates SaaS com orquestração de IA.

Modo Auto do Claude Code

Um segundo modelo Sonnet revê cada chamada de ferramenta do Claude Code antes de ser executada. O que o modo auto bloqueia, o que permite e as regras de permissão que cria nas tuas definições.

Feedback Loops

Passe para o Claude Code um prompt que escreve código, roda o seu teste ou comando de dev, lê a saída, corrige o que quebra e faz loop até a suite ficar verde.

On this page

What You Need Before Starting
Checkout Sessions vs. Payment Intents
Building the Checkout Flow
Webhooks: The Part Everyone Gets Wrong
Customer Portal Setup
Testing the Full Flow
Going Live Checklist
What Build This Now Ships Pre-Built

Pare de configurar. Comece a construir.

Templates SaaS com orquestração de IA.