Use Cases (अपडेट: 1/6/2026)

Claude Code से Stripe Checkout लागू करें: Sessions, Webhooks और Idempotency

Claude Code से Stripe Checkout सुरक्षित लागू करें: hosted checkout, webhooks, metadata, idempotency, test mode और recovery checks.

Claude Code से Stripe Checkout लागू करें: Sessions, Webhooks और Idempotency

Stripe Checkout का मतलब यह नहीं है कि payment integration पूरी तरह खत्म हो गई। यह card form, validation और hosted payment page को Stripe पर ले जाता है, लेकिन order state, access grant, webhook fulfillment, refund और cancellation की जिम्मेदारी अभी भी आपकी application पर रहती है।

Claude Code इस काम में तभी उपयोगी है जब आप उसे छोटे, reviewable tasks दें। “Stripe payment बना दो” कहने के बजाय पहले order schema, फिर environment variable validation, फिर Checkout Route Handler, फिर webhook signature verification, फिर idempotent fulfillment और अंत में security review कराएं।

यह लेख Next.js App Router और Node TypeScript पर आधारित है। code secret key को source में hard-code नहीं करता, पहले internal order बनाता है, फिर Stripe Checkout Session बनाता है, और payment complete होने पर एक idempotent server function से product या subscription access देता है।

Stripe का official flow

Stripe की Checkout Sessions API hosted Checkout, embedded Checkout और custom flows की backend API है। ज्यादातर SaaS, digital products और training payments के लिए hosted checkout सबसे सुरक्षित starting point है क्योंकि payment form, authentication, error handling और payment method UI Stripe संभालता है।

How Checkout works में flow यह है: application Checkout Session बनाती है, user Stripe hosted page पर payment करता है, और checkout.session.completed जैसा webhook event fulfillment trigger करता है। fulfillment का मतलब है payment के बाद खरीदी गई चीज देना, जैसे download access, SaaS plan, workshop seat, shipping task या booking confirmation।

Stripe की Fulfill orders guide साफ कहती है कि webhook जरूरी है और fulfillment function एक ही Checkout Session के लिए कई बार call हो सकता है। Checkout Sessions API reference भी बताती है कि Session एक payment attempt को represent करती है और success के बाद Customer, PaymentIntent या Subscription से जुड़ती है।

पूरी payment architecture के लिए Claude Code payment integration, Claude Code security best practices और Claude Code env management भी देखें।

Architecture

flowchart LR
  Buyer["Buyer"] --> App["Next.js app"]
  App --> CheckoutRoute["/api/checkout"]
  CheckoutRoute --> Orders["Order table"]
  CheckoutRoute --> Stripe["Stripe Checkout Session"]
  Stripe --> Hosted["Hosted Checkout"]
  Hosted --> Webhook["/api/stripe/webhook"]
  Webhook --> Fulfill["fulfillCheckout"]
  Hosted --> Success["/checkout/success"]
  Success --> Fulfill
  Fulfill --> Orders

Checkout route internal order create या reuse करती है और session.url return करती है। Webhook route raw body से Stripe signature verify करती है और paid events को fulfillment में भेजती है। Success page भी वही function call कर सकता है, लेकिन webhook का replacement नहीं है।

Environment और database

npm install stripe zod @prisma/client
npm install --save-dev prisma
npx prisma init

Local development में सिर्फ test mode values रखें। असली sk_test_, sk_live_ या whsec_ values Claude Code prompt में paste न करें।

APP_URL=http://localhost:3000
DATABASE_URL=postgresql://user:password@localhost:5432/checkout_dev
STRIPE_SECRET_KEY=sk_test_xxxxxxxxxxxxxxxxxxxxx
STRIPE_WEBHOOK_SECRET=whsec_xxxxxxxxxxxxxxxxxxxxx
STRIPE_PRICE_EBOOK=price_xxxxxxxxxxxxxxxxxxxxx
STRIPE_PRICE_PRO=price_xxxxxxxxxxxxxxxxxxxxx

Checkout Session से पहले internal order बनाएं। इससे आप client_reference_id और metadata.order_id में stable order ID रख सकते हैं।

// prisma/schema.prisma
datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

generator client {
  provider = "prisma-client-js"
}

model Order {
  id                      String      @id @default(cuid())
  cartId                  String      @unique
  userId                  String
  email                   String?
  productKey              String
  quantity                Int         @default(1)
  status                  OrderStatus @default(PENDING)
  stripeCheckoutSessionId String?     @unique
  stripePaymentIntentId   String?     @unique
  stripeSubscriptionId    String?     @unique
  amountTotal             Int?
  currency                String?
  fulfilledAt             DateTime?
  createdAt               DateTime    @default(now())
  updatedAt               DateTime    @updatedAt
  logs                    FulfillmentLog[]
}

model FulfillmentLog {
  id                      String   @id @default(cuid())
  orderId                 String
  stripeCheckoutSessionId String
  event                   String
  createdAt               DateTime @default(now())
  order                   Order    @relation(fields: [orderId], references: [id])

  @@unique([orderId, event])
  @@index([stripeCheckoutSessionId])
}

enum OrderStatus {
  PENDING
  PAID
  FULFILLED
  CANCELED
  REFUNDED
  FAILED
}

Server utilities

// src/lib/env.ts
import { z } from "zod";

const envSchema = z.object({
  APP_URL: z.string().url(),
  DATABASE_URL: z.string().min(1),
  STRIPE_SECRET_KEY: z.string().regex(/^sk_(test|live)_/),
  STRIPE_WEBHOOK_SECRET: z.string().regex(/^whsec_/),
  STRIPE_PRICE_EBOOK: z.string().startsWith("price_"),
  STRIPE_PRICE_PRO: z.string().startsWith("price_"),
});

export const env = envSchema.parse(process.env);
// src/lib/prisma.ts
import { PrismaClient } from "@prisma/client";

const globalForPrisma = globalThis as unknown as { prisma?: PrismaClient };
export const prisma = globalForPrisma.prisma ?? new PrismaClient();

if (process.env.NODE_ENV !== "production") {
  globalForPrisma.prisma = prisma;
}
// src/lib/stripe.ts
import Stripe from "stripe";
import { env } from "@/lib/env";

export const stripe = new Stripe(env.STRIPE_SECRET_KEY);

Checkout Session बनाना

// app/api/checkout/route.ts
import Stripe from "stripe";
import { NextResponse } from "next/server";
import { z } from "zod";
import { env } from "@/lib/env";
import { prisma } from "@/lib/prisma";
import { stripe } from "@/lib/stripe";

const checkoutRequestSchema = z.object({
  cartId: z.string().min(8),
  userId: z.string().min(1),
  email: z.string().email().optional(),
  productKey: z.enum(["ebook", "pro"]),
});

const priceByProduct = {
  ebook: env.STRIPE_PRICE_EBOOK,
  pro: env.STRIPE_PRICE_PRO,
} as const;

export async function POST(request: Request) {
  const body = await request.json().catch(() => null);
  const parsed = checkoutRequestSchema.safeParse(body);

  if (!parsed.success) {
    return NextResponse.json({ error: "Invalid checkout request" }, { status: 400 });
  }

  const { cartId, userId, email, productKey } = parsed.data;
  const mode: Stripe.Checkout.SessionCreateParams.Mode =
    productKey === "pro" ? "subscription" : "payment";

  const order = await prisma.order.upsert({
    where: { cartId },
    update: {},
    create: { cartId, userId, email, productKey, quantity: 1, status: "PENDING" },
  });

  if (order.stripeCheckoutSessionId) {
    const existing = await stripe.checkout.sessions.retrieve(order.stripeCheckoutSessionId);
    if (existing.status === "open" && existing.url) {
      return NextResponse.json({ url: existing.url, orderId: order.id });
    }

    return NextResponse.json(
      { error: "Checkout session is not open. Create a new cart." },
      { status: 409 },
    );
  }

  const metadata = {
    order_id: order.id,
    cart_id: cartId,
    product_key: productKey,
    user_id: userId,
  };

  const session = await stripe.checkout.sessions.create(
    {
      mode,
      line_items: [{ price: priceByProduct[productKey], quantity: 1 }],
      customer_email: email,
      success_url: `${env.APP_URL}/checkout/success?session_id={CHECKOUT_SESSION_ID}`,
      cancel_url: `${env.APP_URL}/pricing?checkout=cancelled&order_id=${order.id}`,
      client_reference_id: order.id,
      metadata,
      allow_promotion_codes: true,
      payment_intent_data: mode === "payment" ? { metadata } : undefined,
      subscription_data: mode === "subscription" ? { metadata } : undefined,
    },
    { idempotencyKey: `checkout:${order.id}:v1` },
  );

  await prisma.order.update({
    where: { id: order.id },
    data: { stripeCheckoutSessionId: session.id },
  });

  return NextResponse.json({ url: session.url, orderId: order.id });
}

Metadata में सिर्फ internal IDs रखें। email, address या card details न रखें। idempotencyKey Session creation retry के लिए है, webhook fulfillment की duplicate execution रोकने के लिए database guard जरूरी है।

Idempotent fulfillment और webhook

// src/lib/checkout-fulfillment.ts
import Stripe from "stripe";
import { prisma } from "@/lib/prisma";
import { stripe } from "@/lib/stripe";

function stripeObjectId(
  value: string | Stripe.PaymentIntent | Stripe.Subscription | null,
) {
  if (!value) return null;
  return typeof value === "string" ? value : value.id;
}

export async function fulfillCheckout(sessionId: string) {
  const session = await stripe.checkout.sessions.retrieve(sessionId, {
    expand: ["line_items", "payment_intent", "subscription"],
  });

  if (session.payment_status === "unpaid") {
    return { status: "waiting_for_payment" as const, sessionId };
  }

  const orderId = session.metadata?.order_id ?? session.client_reference_id;
  if (!orderId) throw new Error(`Checkout Session ${session.id} has no order_id`);

  return prisma.$transaction(async (tx) => {
    const updated = await tx.order.updateMany({
      where: { id: orderId, fulfilledAt: null },
      data: {
        status: "FULFILLED",
        stripeCheckoutSessionId: session.id,
        stripePaymentIntentId: stripeObjectId(session.payment_intent),
        stripeSubscriptionId: stripeObjectId(session.subscription),
        amountTotal: session.amount_total,
        currency: session.currency,
        fulfilledAt: new Date(),
      },
    });

    if (updated.count === 0) return { status: "already_fulfilled" as const, orderId };

    await tx.fulfillmentLog.create({
      data: { orderId, stripeCheckoutSessionId: session.id, event: "checkout.fulfilled" },
    });

    return { status: "fulfilled" as const, orderId };
  });
}
// app/api/stripe/webhook/route.ts
import Stripe from "stripe";
import { NextResponse } from "next/server";
import { env } from "@/lib/env";
import { prisma } from "@/lib/prisma";
import { stripe } from "@/lib/stripe";
import { fulfillCheckout } from "@/lib/checkout-fulfillment";

export const runtime = "nodejs";

function idFromStripeObject(value: string | { id: string } | null | undefined) {
  if (!value) return null;
  return typeof value === "string" ? value : value.id;
}

async function markSession(sessionId: string, status: "CANCELED" | "FAILED") {
  await prisma.order.updateMany({
    where: { stripeCheckoutSessionId: sessionId, fulfilledAt: null },
    data: { status },
  });
}

export async function POST(request: Request) {
  const signature = request.headers.get("stripe-signature");
  if (!signature) return NextResponse.json({ error: "Missing signature" }, { status: 400 });

  const rawBody = await request.text();
  let event: Stripe.Event;

  try {
    event = stripe.webhooks.constructEvent(rawBody, signature, env.STRIPE_WEBHOOK_SECRET);
  } catch (error) {
    const message = error instanceof Error ? error.message : "Unknown error";
    return NextResponse.json(
      { error: `Webhook signature verification failed: ${message}` },
      { status: 400 },
    );
  }

  switch (event.type) {
    case "checkout.session.completed":
    case "checkout.session.async_payment_succeeded":
      await fulfillCheckout((event.data.object as Stripe.Checkout.Session).id);
      break;
    case "checkout.session.async_payment_failed":
      await markSession((event.data.object as Stripe.Checkout.Session).id, "FAILED");
      break;
    case "checkout.session.expired":
      await markSession((event.data.object as Stripe.Checkout.Session).id, "CANCELED");
      break;
    case "charge.refunded": {
      const charge = event.data.object as Stripe.Charge;
      const paymentIntentId = idFromStripeObject(charge.payment_intent);
      if (paymentIntentId) {
        await prisma.order.updateMany({
          where: { stripePaymentIntentId: paymentIntentId },
          data: { status: "REFUNDED" },
        });
      }
      break;
    }
    case "customer.subscription.deleted": {
      const subscription = event.data.object as Stripe.Subscription;
      await prisma.order.updateMany({
        where: { stripeSubscriptionId: subscription.id },
        data: { status: "CANCELED" },
      });
      break;
    }
  }

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

Webhook में raw body जरूरी है। अगर Claude Code इसे signature verification से पहले request.json() में बदल दे, तो इसे security bug मानें।

Test mode, use cases और pitfalls

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

CLI से मिला whsec_ STRIPE_WEBHOOK_SECRET में डालें, Next.js restart करें और test card 4242 4242 4242 4242 से payment करें। Stripe Dashboard के साथ database भी देखें: Order.status FULFILLED होना चाहिए, Stripe IDs save होने चाहिए, और success page refresh करने पर दूसरा fulfillment log नहीं बनना चाहिए।

Use caseCheckout modeFulfillmentRisk
PDF या templatepaymentDownload access देनाSuccess page पर ही file न दें
SaaS Pro plansubscriptionPlan और seats activate करनाSubscription deletion handle करें
Training या consultation depositpaymentSlot reserve करनाRefund से slot release होगा या नहीं तय करें

Common failures: सिर्फ success_url पर access देना, metadata.order_id भूलना, Stripe idempotencyKey को webhook idempotency समझ लेना, test और live objects mix करना, और refund के बाद app permissions update न करना।

Claude Code review prompt छोटा रखें।

Review only the Stripe Checkout implementation.
Check env-only secrets, metadata order_id without PII, raw-body webhook signature verification,
idempotent fulfillment, test-mode setup, refund and cancellation handling.
Return concrete file and line references.

असल में try करते समय confirmation points

Publish से पहले test-mode payment करें, webhook log देखें, order table inspect करें, success page reload करें, Session expire करें, refund करें और test subscription cancel करें। अगर वही Checkout Session कई बार process होने पर भी access सिर्फ एक बार मिलता है, तो production review के लिए आधार तैयार है।

ClaudeCodeLab Stripe Checkout implementation review, Claude Code payment workflow training, और आपके order, refund और subscription rules के अनुसार implementation support दे सकता है।

#Claude Code #Stripe #payments #Checkout #TypeScript
मुफ़्त

मुफ़्त PDF: Claude Code cheatsheet

Email डालें और commands, review habits तथा safe workflow वाली एक-page PDF पाएँ.

हम आपका data सुरक्षित रखते हैं और spam नहीं भेजते.

Masa

लेखक के बारे में

Masa

Claude Code workflow और team adoption पर काम करने वाला engineer.