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

Claude Code से Stripe सब्सक्रिप्शन लागू करें

Claude Code से Stripe सब्सक्रिप्शन बनाएं: Checkout, Portal, Webhook, entitlement table और SaaS कमाई।

Claude Code से Stripe सब्सक्रिप्शन लागू करें

सब्सक्रिप्शन सिर्फ भुगतान बटन नहीं, पूरा revenue flow है

Stripe Checkout से subscription शुरू करना आसान है। असली काम उसके बाद आता है: payment fail हो जाए तो क्या होगा, plan बदले तो access कैसे बदलेगा, cancellation period के अंत में होगा या तुरंत, invoice कौन देखेगा, customer payment method कैसे बदलेगा, Webhook दोबारा आए तो क्या होगा, और paid feature कब बंद होंगे।

Claude Code को केवल “Stripe लगा दो” कहने से demo code मिल सकता है, production-ready billing system नहीं। बेहतर तरीका है कि Claude Code को official docs, database schema, access rules, local test commands और review checklist साथ में दी जाए। तब agent अनुमान कम लगाता है और code business rule से जुड़ता है।

मुख्य मॉडल यह है: Stripe billing और subscription status की source of truth है। आपकी app database यह तय करती है कि user अभी कौन सा feature use कर सकता है। active और trialing access खोल सकते हैं, past_due पर छोटी grace period मिल सकती है, और unpaid, canceled, paused पर paid access बंद होना चाहिए।

flowchart LR
  A["Pricing page"] --> B["Checkout Session<br/>mode=subscription"]
  B --> C["Stripe subscription"]
  C --> D["Signed webhook"]
  D --> E["billing_subscriptions"]
  D --> F["entitlements"]
  F --> G["Feature gate"]
  G --> H["SaaS, course, template library"]
  H --> I["Customer Portal"]
  I --> C

पहले pricing और access design करें

Code लिखने से पहले plan table बनाएं। इससे monetization logic React component, API route और support email में अलग-अलग नहीं फैलता।

PlanTarget userMonthly exampleAnnual exampleAccess
Freecontent try करने वाले visitors₹0₹0preview, sample downloads
Prosolo builders, template buyers₹1,499₹14,999full articles, templates, course library
Studioteams और recurring clients₹7,999₹79,999Pro access, team seats, review requests, training material

कम से कम 3 concrete use cases लिखें। पहला, micro-SaaS जहां paid plan dashboard export, automation और analytics खोलता है। दूसरा, info-product funnel जहां free article से template library और फिर membership तक user जाता है। तीसरा, training या consulting offer जहां team seats, recurring review slots और invoice handling जरूरी होते हैं। ये use cases बताती हैं कि payment failure, cancellation और entitlement revoke को कैसे handle करना है।

entitlement का सरल अर्थ है feature permission, जैसे templates:download या team:seatsdunning payment fail होने के बाद recovery process है: reminder email, retry rules और payment method update करने का link। observability का मतलब है कि आप देख सकें कि किस Stripe event ने किस user का plan और access बदला।

Claude Code prompt में official docs दें

Stripe details समय के साथ बदल सकती हैं। इसलिए provider-specific claim official docs से verify करें।

Next.js App Router app के लिए Stripe Billing subscriptions implement करें।
इन official docs को source of truth मानें:
- Checkout Sessions API: https://docs.stripe.com/api/checkout/sessions/create
- Customer Portal: https://docs.stripe.com/customer-management
- Subscription webhooks: https://docs.stripe.com/billing/subscriptions/webhooks
- Webhooks and local testing: https://docs.stripe.com/webhooks
- Subscription object and statuses: https://docs.stripe.com/api/subscriptions/object

Requirements:
- TypeScript, Next.js App Router, Postgres
- Checkout mode="subscription" use करे
- Customer Portal payment method, invoices, plan changes और cancellation handle करे
- Webhook Stripe signature verify करे और event_id से duplicate रोके
- Local entitlements table app access decide करे
- active/trialing access दें, past_due को 3 दिन grace, unpaid/canceled/paused revoke
- Copy-paste code और stripe listen commands शामिल करें

Database: billing state और app access अलग रखें

यह Postgres schema minimal लेकिन practical है। billing_subscriptions Stripe status रखता है, entitlements app feature access रखता है, और webhook_events duplicate events से बचाता है।

create table if not exists app_users (
  id text primary key,
  email text not null unique,
  stripe_customer_id text unique,
  created_at timestamptz not null default now()
);

create table if not exists billing_subscriptions (
  user_id text primary key references app_users(id) on delete cascade,
  stripe_customer_id text not null,
  stripe_subscription_id text not null unique,
  plan_key text not null,
  status text not null check (
    status in ('incomplete', 'incomplete_expired', 'trialing', 'active', 'past_due', 'canceled', 'unpaid', 'paused')
  ),
  access_state text not null check (access_state in ('pending', 'granted', 'grace', 'revoked')),
  current_period_end timestamptz,
  cancel_at_period_end boolean not null default false,
  grace_until timestamptz,
  updated_at timestamptz not null default now()
);

create table if not exists entitlements (
  user_id text not null references app_users(id) on delete cascade,
  feature_key text not null,
  active boolean not null default true,
  expires_at timestamptz,
  updated_at timestamptz not null default now(),
  primary key (user_id, feature_key)
);

create table if not exists webhook_events (
  event_id text primary key,
  event_type text not null,
  status text not null default 'processing',
  attempts integer not null default 1,
  processed_at timestamptz,
  last_error text,
  created_at timestamptz not null default now()
);

Checkout और Customer Portal code

Demo में x-demo-user-id header use किया गया है। Production में इसे अपने real auth system से बदलें।

npm install stripe postgres
STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
STRIPE_PRO_PRICE_ID=price_...
STRIPE_STUDIO_PRICE_ID=price_...
NEXT_PUBLIC_APP_URL=http://localhost:3000
DATABASE_URL=postgres://user:pass@localhost:5432/app
STRIPE_AUTOMATIC_TAX=false
// src/lib/db.ts
import postgres from "postgres";

export const sql = postgres(process.env.DATABASE_URL!, {
  max: 5,
  idle_timeout: 20,
});
// src/lib/billing.ts
import Stripe from "stripe";
import { sql } from "@/lib/db";

export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
const APP_URL = process.env.NEXT_PUBLIC_APP_URL ?? "http://localhost:3000";

export const PLANS = {
  free: { priceId: null, features: ["article:preview"] },
  pro: {
    priceId: process.env.STRIPE_PRO_PRICE_ID!,
    features: ["article:full", "templates:download", "course:library"],
  },
  studio: {
    priceId: process.env.STRIPE_STUDIO_PRICE_ID!,
    features: ["article:full", "templates:download", "course:library", "team:seats", "review:request"],
  },
} as const;

export type PlanKey = keyof typeof PLANS;
export type PaidPlanKey = Exclude<PlanKey, "free">;

export function isPaidPlanKey(value: unknown): value is PaidPlanKey {
  return value === "pro" || value === "studio";
}

export async function createCheckoutSession(userId: string, planKey: PaidPlanKey) {
  const customerId = await findOrCreateCustomer(userId);
  const plan = PLANS[planKey];

  const session = await stripe.checkout.sessions.create({
    customer: customerId,
    mode: "subscription",
    line_items: [{ price: plan.priceId, quantity: 1 }],
    client_reference_id: userId,
    allow_promotion_codes: true,
    automatic_tax: { enabled: process.env.STRIPE_AUTOMATIC_TAX === "true" },
    success_url: `${APP_URL}/billing/success?session_id={CHECKOUT_SESSION_ID}`,
    cancel_url: `${APP_URL}/pricing`,
    subscription_data: { metadata: { userId, planKey } },
    metadata: { userId, planKey },
  });

  if (!session.url) throw new Error("Stripe did not return a Checkout URL");
  return session.url;
}

export async function createPortalSession(userId: string) {
  const customerId = await getCustomerId(userId);
  const session = await stripe.billingPortal.sessions.create({
    customer: customerId,
    return_url: `${APP_URL}/settings/billing`,
  });
  return session.url;
}

async function findOrCreateCustomer(userId: string) {
  const users = await sql`select id, email, stripe_customer_id from app_users where id = ${userId}`;
  const user = users[0] as { id: string; email: string; stripe_customer_id: string | null } | undefined;
  if (!user) throw new Error("User not found");
  if (user.stripe_customer_id) return user.stripe_customer_id;

  const customer = await stripe.customers.create({ email: user.email, metadata: { userId } });
  await sql`update app_users set stripe_customer_id = ${customer.id} where id = ${userId}`;
  return customer.id;
}

async function getCustomerId(userId: string) {
  const rows = await sql`select stripe_customer_id from app_users where id = ${userId}`;
  const customerId = rows[0]?.stripe_customer_id as string | undefined;
  if (!customerId) throw new Error("Stripe customer is not linked");
  return customerId;
}
// src/app/api/billing/checkout/route.ts
import { NextRequest, NextResponse } from "next/server";
import { createCheckoutSession, isPaidPlanKey } from "@/lib/billing";

export async function POST(request: NextRequest) {
  const userId = request.headers.get("x-demo-user-id");
  if (!userId) return NextResponse.json({ error: "Missing x-demo-user-id" }, { status: 401 });

  const { planKey } = await request.json();
  if (!isPaidPlanKey(planKey)) return NextResponse.json({ error: "Invalid plan" }, { status: 400 });

  const url = await createCheckoutSession(userId, planKey);
  return NextResponse.json({ url });
}
// src/app/api/billing/portal/route.ts
import { NextRequest, NextResponse } from "next/server";
import { createPortalSession } from "@/lib/billing";

export async function POST(request: NextRequest) {
  const userId = request.headers.get("x-demo-user-id");
  if (!userId) return NextResponse.json({ error: "Missing x-demo-user-id" }, { status: 401 });

  const url = await createPortalSession(userId);
  return NextResponse.json({ url });
}

Webhook से entitlement sync करें

Success URL payment proof नहीं है। यह सिर्फ user को app में वापस लाने का route है। Paid access signed Webhook या Stripe API से read किए गए latest subscription state से दें। Stripe events order में आएंगे, यह मानकर code न लिखें; subscription को फिर से retrieve करें।

// src/lib/entitlements.ts
import Stripe from "stripe";
import { sql } from "@/lib/db";
import { PLANS, PlanKey, stripe } from "@/lib/billing";

type AccessState = "pending" | "granted" | "grace" | "revoked";

export async function syncSubscriptionFromStripe(subscriptionId: string) {
  const subscription = await stripe.subscriptions.retrieve(subscriptionId, { expand: ["items.data.price"] });
  const item = subscription.items.data[0];
  const planKey = planKeyFromPrice(item?.price.id);
  const userId = subscription.metadata.userId;
  if (!userId) throw new Error(`Subscription ${subscription.id} has no userId metadata`);

  const accessState = accessStateFor(subscription.status);
  const currentPeriodEnd = item?.current_period_end ? new Date(item.current_period_end * 1000) : null;
  const graceUntil = accessState === "grace" ? new Date(Date.now() + 3 * 24 * 60 * 60 * 1000) : null;

  await sql.begin(async (tx) => {
    await tx`
      insert into billing_subscriptions (
        user_id, stripe_customer_id, stripe_subscription_id, plan_key,
        status, access_state, current_period_end, cancel_at_period_end, grace_until, updated_at
      )
      values (
        ${userId}, ${subscription.customer as string}, ${subscription.id}, ${planKey},
        ${subscription.status}, ${accessState}, ${currentPeriodEnd},
        ${subscription.cancel_at_period_end}, ${graceUntil}, now()
      )
      on conflict (user_id) do update set
        stripe_customer_id = excluded.stripe_customer_id,
        stripe_subscription_id = excluded.stripe_subscription_id,
        plan_key = excluded.plan_key,
        status = excluded.status,
        access_state = excluded.access_state,
        current_period_end = excluded.current_period_end,
        cancel_at_period_end = excluded.cancel_at_period_end,
        grace_until = excluded.grace_until,
        updated_at = now()
    `;

    const features = accessState === "granted" || accessState === "grace" ? PLANS[planKey].features : PLANS.free.features;
    await tx`delete from entitlements where user_id = ${userId}`;
    for (const feature of features) {
      await tx`insert into entitlements (user_id, feature_key, active, expires_at, updated_at) values (${userId}, ${feature}, true, ${graceUntil}, now())`;
    }
  });

  console.info("stripe.subscription.synced", { userId, subscriptionId: subscription.id, status: subscription.status, accessState, planKey });
}

function planKeyFromPrice(priceId: string | undefined): PlanKey {
  const entry = Object.entries(PLANS).find(([, plan]) => plan.priceId === priceId);
  return entry ? (entry[0] as PlanKey) : "free";
}

function accessStateFor(status: Stripe.Subscription.Status): AccessState {
  if (status === "active" || status === "trialing") return "granted";
  if (status === "past_due") return "grace";
  if (status === "incomplete") return "pending";
  return "revoked";
}
// src/app/api/webhooks/stripe/route.ts
import { NextRequest, NextResponse } from "next/server";
import Stripe from "stripe";
import { sql } from "@/lib/db";
import { stripe } from "@/lib/billing";
import { syncSubscriptionFromStripe } from "@/lib/entitlements";

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

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

  const rows = await sql`
    insert into webhook_events (event_id, event_type, status)
    values (${event.id}, ${event.type}, 'processing')
    on conflict (event_id) do nothing
    returning event_id
  `;
  if (rows.length === 0) return NextResponse.json({ received: true, duplicate: true });

  if (event.type === "checkout.session.completed") {
    const session = event.data.object as Stripe.Checkout.Session;
    const subscriptionId = typeof session.subscription === "string" ? session.subscription : session.subscription?.id;
    if (subscriptionId) await syncSubscriptionFromStripe(subscriptionId);
  }

  if (event.type === "customer.subscription.created" || event.type === "customer.subscription.updated" || event.type === "customer.subscription.deleted") {
    const subscription = event.data.object as Stripe.Subscription;
    await syncSubscriptionFromStripe(subscription.id);
  }

  if (event.type === "invoice.paid" || event.type === "invoice.payment_failed") {
    const invoice = event.data.object as Stripe.Invoice;
    const value = invoice.parent?.subscription_details?.subscription as string | { id: string } | null | undefined;
    const subscriptionId = typeof value === "string" ? value : value?.id;
    if (subscriptionId) await syncSubscriptionFromStripe(subscriptionId);
  }

  await sql`update webhook_events set status = 'processed', processed_at = now() where event_id = ${event.id}`;
  return NextResponse.json({ received: true });
}

Local Webhook testing

stripe listen से मिले whsec_... value को STRIPE_WEBHOOK_SECRET में रखें।

stripe listen --events checkout.session.completed,customer.subscription.updated,customer.subscription.deleted,invoice.paid,invoice.payment_failed --forward-to localhost:3000/api/webhooks/stripe
stripe trigger checkout.session.completed
stripe trigger invoice.payment_failed

CLI trigger smoke test के लिए उपयोगी हैं। Final test के लिए test mode Product और Price बनाकर browser से Checkout पूरा करें, फिर Customer Portal में card update, plan change और cancellation test करें।

आम गलतियां

Success URL देखकर paid feature न खोलें। User वापस न आए, payment extra authentication में अटक जाए या Webhook देर से आए, यह सब सामान्य है।

userId को केवल Checkout Session metadata में न रखें। अगर Subscription से sync करना है तो subscription_data.metadata में भी लिखें।

past_due, unpaid और canceled को एक जैसा न मानें। past_due पर छोटी grace period ठीक हो सकती है, पर unpaid और canceled पर paid access सामान्यतः बंद करें।

Customer Portal API route बनाने के बाद Stripe Dashboard configuration न भूलें। कौन से products बदल सकते हैं, cancellation allowed है या नहीं, payment method update दिखेगा या नहीं, यह Dashboard में तय होता है।

Tax, currency, refund और annual discount को launch week तक न टालें। Stripe automation देता है, लेकिन business policy आपको तय करनी होती है।

Monetization CTA

ClaudeCodeLab में content और template funnel पर इस flow को आजमाते समय सबसे बड़ा फायदा code generation से नहीं, state table पहले तय करने से मिला। जब active, past_due, unpaid और canceled का app access साफ होता है, Claude Code prompt, tests, analytics और support message एक ही नियम पर चलते हैं।

आगे पढ़ें: Claude Code payment integration, Claude Code authentication implementation, और Claude Code analytics implementation। अगर आप अपने SaaS, course site या product funnel में Stripe Billing, entitlement table और Claude Code review workflow लागू करना चाहते हैं, तो Claude Code training and consultation या products and templates देखें।

Implementation से पहले official docs फिर देखें: Checkout Sessions API, Customer Portal, subscription webhooks, webhooks, subscription statuses, और Claude Code setup

#Claude Code #Stripe #subscription #SaaS #TypeScript
मुफ़्त

मुफ़्त PDF: Claude Code cheatsheet

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

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

Masa

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

Masa

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