Claude Code for Stripe Checkout: Payment Link and Hosted Pages — Claude Skills 360 Blog
Blog / Backend / Claude Code for Stripe Checkout: Payment Link and Hosted Pages
Backend

Claude Code for Stripe Checkout: Payment Link and Hosted Pages

Published: April 19, 2027
Read time: 7 min read
By: Claude Skills 360

Stripe Checkout is a hosted payment page that handles card input, validation, 3D Secure, and receipts — stripe.checkout.sessions.create({ line_items, mode, success_url, cancel_url }) creates a checkout session. Redirect to session.url sends customers to Stripe’s hosted page. mode: "payment" for one-time purchases; mode: "subscription" for recurring billing. line_items reference Price IDs from your Stripe dashboard or dynamically generated prices. metadata stores your order ID for webhook correlation. checkout.session.completed webhook fires when payment succeeds — retrieve session with stripe.checkout.sessions.retrieve(session.id, { expand: ["line_items"] }). Embedded checkout renders in your page with @stripe/stripe-js mountStripeElement. allow_promotion_codes: true enables coupon input. customer_creation: "always" creates Stripe Customer records. Claude Code generates checkout session creation, webhook handlers, embedded checkout React components, and subscription upgrade flows.

CLAUDE.md for Stripe Checkout

## Stripe Checkout Stack
- Version: stripe >= 14.25 (Node), @stripe/stripe-js >= 3.4 (browser)
- Session: stripe.checkout.sessions.create({ line_items: [{ price, quantity }], mode: "payment", success_url, cancel_url })
- Redirect: return Response.redirect(session.url) — or return { url: session.url }
- Webhook: stripe.webhooks.constructEvent(rawBody, sig, secret) → checkout.session.completed
- Retrieve: stripe.checkout.sessions.retrieve(id, { expand: ["line_items", "customer"] })
- Subscription: mode: "subscription", line_items: [{ price: "price_xxx", quantity: 1 }]
- Metadata: metadata: { orderId, userId } — accessible in webhook
- Embedded: stripe.initEmbeddedCheckout({ clientSecret }) + EmbeddedCheckout React component

Checkout Session Creation

// lib/stripe/checkout.ts — session creation utilities
import Stripe from "stripe"

export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
  apiVersion: "2024-04-10",
  typescript: true,
})

// One-time payment checkout
export async function createPaymentCheckout(params: {
  lineItems: { priceId: string; quantity: number }[]
  userId: string
  orderId: string
  customerEmail?: string
  successUrl: string
  cancelUrl: string
  metadata?: Record<string, string>
}) {
  const session = await stripe.checkout.sessions.create({
    mode: "payment",
    line_items: params.lineItems.map(item => ({
      price: item.priceId,
      quantity: item.quantity,
    })),
    success_url: `${params.successUrl}?session_id={CHECKOUT_SESSION_ID}`,
    cancel_url: params.cancelUrl,
    customer_email: params.customerEmail,
    metadata: {
      userId: params.userId,
      orderId: params.orderId,
      ...params.metadata,
    },
    // Payment options
    allow_promotion_codes: true,
    billing_address_collection: "auto",
    shipping_address_collection: {
      allowed_countries: ["US", "CA", "GB", "AU"],
    },
    payment_intent_data: {
      capture_method: "automatic",
      metadata: { orderId: params.orderId },
    },
    // Receipt email
    payment_method_options: {
      card: { request_three_d_secure: "automatic" },
    },
    // Tax calculation
    automatic_tax: { enabled: true },
    tax_id_collection: { enabled: true },
  })

  return session
}

// Subscription checkout
export async function createSubscriptionCheckout(params: {
  priceId: string
  userId: string
  customerEmail?: string
  successUrl: string
  cancelUrl: string
  trialDays?: number
  existingCustomerId?: string
}) {
  const session = await stripe.checkout.sessions.create({
    mode: "subscription",
    line_items: [{ price: params.priceId, quantity: 1 }],
    success_url: `${params.successUrl}?session_id={CHECKOUT_SESSION_ID}`,
    cancel_url: params.cancelUrl,
    customer: params.existingCustomerId,
    customer_email: !params.existingCustomerId ? params.customerEmail : undefined,
    customer_creation: params.existingCustomerId ? undefined : "always",
    metadata: { userId: params.userId },
    subscription_data: {
      trial_period_days: params.trialDays,
      metadata: { userId: params.userId },
    },
    allow_promotion_codes: true,
    billing_address_collection: "required",
  })

  return session
}

// Retrieve completed session with expanded data
export async function getCompletedSession(sessionId: string) {
  return stripe.checkout.sessions.retrieve(sessionId, {
    expand: ["line_items", "line_items.data.price.product", "customer", "subscription"],
  })
}

API Routes

// app/api/checkout/route.ts — create checkout session
import { NextRequest, NextResponse } from "next/server"
import { auth } from "@clerk/nextjs/server"
import { createPaymentCheckout, createSubscriptionCheckout } from "@/lib/stripe/checkout"
import { db } from "@/lib/db"
import { z } from "zod"

const CheckoutSchema = z.discriminatedUnion("type", [
  z.object({
    type: z.literal("payment"),
    orderId: z.string(),
    items: z.array(z.object({ priceId: z.string(), quantity: z.number().int().positive() })),
  }),
  z.object({
    type: z.literal("subscription"),
    priceId: z.string(),
    trialDays: z.number().int().optional(),
  }),
])

export async function POST(request: NextRequest) {
  const { userId } = await auth()
  if (!userId) return NextResponse.json({ error: "Unauthorized" }, { status: 401 })

  const body = CheckoutSchema.safeParse(await request.json())
  if (!body.success) return NextResponse.json({ error: "Invalid request" }, { status: 400 })

  const user = await db.user.findUniqueOrThrow({ where: { clerkId: userId } })
  const origin = request.headers.get("origin") ?? process.env.NEXT_PUBLIC_APP_URL!

  try {
    if (body.data.type === "payment") {
      const session = await createPaymentCheckout({
        lineItems: body.data.items,
        userId,
        orderId: body.data.orderId,
        customerEmail: user.email,
        successUrl: `${origin}/checkout/success`,
        cancelUrl: `${origin}/cart`,
      })
      return NextResponse.json({ url: session.url })
    }

    if (body.data.type === "subscription") {
      const session = await createSubscriptionCheckout({
        priceId: body.data.priceId,
        userId,
        customerEmail: user.email,
        existingCustomerId: user.stripeCustomerId ?? undefined,
        successUrl: `${origin}/dashboard?upgraded=true`,
        cancelUrl: `${origin}/pricing`,
        trialDays: body.data.trialDays,
      })
      return NextResponse.json({ url: session.url })
    }
  } catch (err) {
    console.error("[Checkout] Error:", err)
    return NextResponse.json({ error: "Checkout creation failed" }, { status: 500 })
  }
}

Webhook Handler

// app/api/webhooks/stripe/route.ts — checkout.session.completed
import { NextRequest, NextResponse } from "next/server"
import { stripe, getCompletedSession } from "@/lib/stripe/checkout"
import { db } from "@/lib/db"

export async function POST(request: NextRequest) {
  const body = await request.text()
  const sig = request.headers.get("stripe-signature")!

  let event: Stripe.Event
  try {
    event = stripe.webhooks.constructEvent(body, sig, process.env.STRIPE_WEBHOOK_SECRET!)
  } catch {
    return NextResponse.json({ error: "Invalid signature" }, { status: 400 })
  }

  // Idempotency check
  const processed = await db.stripeEvent.findUnique({ where: { eventId: event.id } })
  if (processed) return NextResponse.json({ ok: true })

  await db.stripeEvent.create({ data: { eventId: event.id, type: event.type } })

  switch (event.type) {
    case "checkout.session.completed": {
      const session = event.data.object as Stripe.Checkout.Session
      if (session.payment_status !== "paid") break

      const fullSession = await getCompletedSession(session.id)
      const userId = session.metadata?.userId

      if (session.mode === "payment") {
        await db.order.update({
          where: { id: session.metadata?.orderId },
          data: {
            status: "paid",
            stripeSessionId: session.id,
            stripePaymentIntentId: session.payment_intent as string,
          },
        })
      }

      if (session.mode === "subscription" && userId) {
        const subscription = fullSession.subscription as Stripe.Subscription
        await db.user.update({
          where: { clerkId: userId },
          data: {
            stripeCustomerId: session.customer as string,
            subscriptionId: subscription.id,
            subscriptionStatus: subscription.status,
            plan: "pro",
          },
        })
      }
      break
    }

    case "checkout.session.expired": {
      const session = event.data.object as Stripe.Checkout.Session
      if (session.metadata?.orderId) {
        await db.order.update({
          where: { id: session.metadata.orderId },
          data: { status: "expired" },
        })
      }
      break
    }
  }

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

export const config = { api: { bodyParser: false } }

Checkout Button Component

// components/checkout/CheckoutButton.tsx — trigger checkout
"use client"
import { useState } from "react"
import { useRouter } from "next/navigation"

interface CheckoutButtonProps {
  priceId?: string
  orderId?: string
  items?: { priceId: string; quantity: number }[]
  type: "payment" | "subscription"
  className?: string
  children: React.ReactNode
}

export function CheckoutButton({ priceId, orderId, items, type, className, children }: CheckoutButtonProps) {
  const [loading, setLoading] = useState(false)
  const router = useRouter()

  const handleCheckout = async () => {
    setLoading(true)
    try {
      const body = type === "subscription"
        ? { type: "subscription", priceId }
        : { type: "payment", orderId, items }

      const res = await fetch("/api/checkout", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(body),
      })

      const { url, error } = await res.json()
      if (error) throw new Error(error)

      router.push(url)
    } catch (err) {
      console.error(err)
    } finally {
      setLoading(false)
    }
  }

  return (
    <button onClick={handleCheckout} disabled={loading} className={className ?? "btn-primary"}>
      {loading ? "Loading..." : children}
    </button>
  )
}

For the Stripe Elements alternative when a custom-styled embedded payment form within your own page UI is preferred over redirecting to Stripe’s hosted Checkout page — Stripe Elements with CardElement or PaymentElement keep customers on your site throughout checkout, see the Stripe Elements guide. For the Stripe Payment Links alternative when no-code payment links shared via email or social media (without building a checkout flow) are sufficient — Payment Links require zero development and work for simple product sales, though unlike Checkout Sessions they lack server-side metadata and webhook customization, see the Stripe dashboard docs. The Claude Skills 360 bundle includes Stripe Checkout skill sets covering sessions, webhooks, and subscription upgrades. Start with the free tier to try payment integration generation.

Keep Reading

Backend

Claude Code for Bun: Fast JavaScript Runtime and Toolkit

Build with Bun and Claude Code — Bun.serve for HTTP servers, Bun.file for fast file I/O, Bun.$ for shell commands, Bun.sql for SQLite and PostgreSQL, Bun.build for bundling, bun:test for testing, Bun.hash for hashing, bun.lock for deterministic installs, bun run for package.json scripts, hot reloading with --hot, bun init for project scaffolding, and compatibility with Node.js modules.

6 min read Jun 13, 2027
Backend

Claude Code for Express.js Advanced: Patterns for Production APIs

Advanced Express.js patterns with Claude Code — typed request handlers with RequestHandler generics, async error handling middleware, Zod validation middleware factory, rate limiting with express-rate-limit and Redis store, helmet security middleware, compression, dependency injection with tsyringe, file upload with multer and S3, pagination utilities, JWT middleware, and structured logging with pino.

6 min read Jun 8, 2027
Backend

Claude Code for KeystoneJS: Node.js CMS and App Framework

Build full-stack apps with KeystoneJS and Claude Code — config with lists, fields.text and fields.relationship for schema definition, access control with isAuthenticated and isAdmin functions, hooks with beforeOperation and afterOperation, GraphQL API auto-generation from schema, AdminUI for content management, session with statelessSessions, Prisma adapter for database, file storage with images and files fields, and custom REST endpoints.

6 min read Jun 7, 2027

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free