agentsclimarketplace

Stripe

Skill GlamgarOnDiscord/claude-saas-blueprint/.claude/skills/stripe

Plug-and-play AI workflow that turns Claude Code, Cursor, Copilot or any AI agent into a senior SaaS engineer. Hexagonal architecture, multi-tenant by default, 31 skills + APEX workflow.

Install
npx -y skills add GlamgarOnDiscord/claude-saas-blueprint --skill stripe

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its author says it does

Copied from the file, not written here

Intégration Stripe complète pour SaaS : Checkout, Subscriptions, Webhooks, Customer Portal. Best practices officielles, prêt pour la production.

SKILL.md

9.7 KB, as published. Nobody here has run it

Arguments

  • mode : checkout | webhooks | subscriptions | portal | setup | full

/stripe setup — Installation & configuration initiale

pnpm add stripe @stripe/stripe-js
pnpm add -D @types/stripe

Variables .env.example à ajouter :

NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...
STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...   # depuis: stripe listen --forward-to ...

Client Stripe singleton src/adapters/payments/stripe.ts :

import Stripe from 'stripe'
export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
  apiVersion: '2026-03-31',   // toujours pin la version API (verifier sur stripe.com)
  typescript: true,
})

CLI local — écouter les webhooks :

brew install stripe/stripe-cli/stripe
stripe login
stripe listen --forward-to http://localhost:3000/api/webhooks/stripe
# Copier le whsec_... dans .env.local

Tester des events :

stripe trigger invoice.paid
stripe trigger customer.subscription.created
stripe trigger invoice.payment_failed
stripe trigger checkout.session.completed

📖 Docs : https://docs.stripe.com/stripe-cli


/stripe checkout — Checkout Session (recommandé pour SaaS)

Utiliser Checkout Sessions (pas Payment Intents) sauf besoin de contrôle granulaire. Checkout gère automatiquement : taxes, promos, subscriptions, 3D Secure.

Route API app/api/checkout/route.ts :

import { stripe } from '@/adapters/payments/stripe'
import { auth } from '@/adapters/auth'
import { NextResponse } from 'next/server'
import { z } from 'zod'

const Body = z.object({ priceId: z.string() })

export async function POST(req: Request) {
  const session = await auth()
  if (!session) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })

  const { priceId } = Body.parse(await req.json())

  const checkout = await stripe.checkout.sessions.create({
    customer_email: session.user.email,
    client_reference_id: session.user.id,   // lier à votre user
    line_items: [{ price: priceId, quantity: 1 }],
    mode: 'subscription',
    success_url: `${process.env.NEXT_PUBLIC_APP_URL}/billing?success=1`,
    cancel_url:  `${process.env.NEXT_PUBLIC_APP_URL}/billing?canceled=1`,
    automatic_tax: { enabled: true },
    billing_address_collection: 'auto',
    allow_promotion_codes: true,
  })

  return NextResponse.json({ data: { url: checkout.url } })
}

Côté client :

const res = await fetch('/api/checkout', {
  method: 'POST',
  body: JSON.stringify({ priceId: 'price_xxx' }),
})
const { data } = await res.json()
window.location.href = data.url

Cartes de test :

CarteRésultat
4242 4242 4242 4242Succès
4000 0000 0000 0002Refusée
4000 0025 0000 31553D Secure requis
4000 0000 0000 9995Fonds insuffisants

📖 Docs : https://docs.stripe.com/payments/checkout-sessions-and-payment-intents-comparison


/stripe webhooks — Webhooks (critique pour SaaS)

⚠️ Utiliser le body RAW (pas parsé en JSON) pour la vérification de signature. Stocker event.id en DB pour l'idempotence (éviter les doubles exécutions).

Route app/api/webhooks/stripe/route.ts :

import { stripe } from '@/adapters/payments/stripe'
import { headers } from 'next/headers'
import { NextResponse } from 'next/server'

// CRITIQUE : désactiver le body parser de Next.js
export const config = { api: { bodyParser: false } }

export async function POST(req: Request) {
  const body      = await req.text()          // RAW — pas req.json()
  const signature = headers().get('stripe-signature')!

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

  // Idempotence : ignorer si déjà traité
  const alreadyProcessed = await db.stripeEvents.findUnique({ where: { id: event.id } })
  if (alreadyProcessed) return NextResponse.json({ received: true })

  try {
    switch (event.type) {
      case 'checkout.session.completed':
        await handleCheckoutCompleted(event.data.object as Stripe.Checkout.Session)
        break

      case 'invoice.paid':
        // Source de vérité pour les subscriptions actives
        await handleInvoicePaid(event.data.object as Stripe.Invoice)
        break

      case 'invoice.payment_failed':
        await handlePaymentFailed(event.data.object as Stripe.Invoice)
        break

      case 'customer.subscription.updated':
        await handleSubscriptionUpdated(event.data.object as Stripe.Subscription)
        break

      case 'customer.subscription.deleted':
        await handleSubscriptionCanceled(event.data.object as Stripe.Subscription)
        break
    }

    // Marquer comme traité
    await db.stripeEvents.create({ data: { id: event.id, type: event.type } })
  } catch (err) {
    // Retourner 500 → Stripe retentera automatiquement
    return NextResponse.json({ error: 'Handler failed' }, { status: 500 })
  }

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

Events critiques à écouter (SaaS) :

EventAction
checkout.session.completedCréer/lier customer + subscription en DB
invoice.paid✅ Activer l'accès / renouveler
invoice.payment_failed⚠️ Restreindre l'accès, notifier
customer.subscription.updatedSync plan en DB
customer.subscription.deletedDésactiver l'accès

📖 Docs : https://docs.stripe.com/webhooks/handling-payment-events | https://docs.stripe.com/webhooks/signature


/stripe subscriptions — Gestion des abonnements

États possibles :

trialing   → Essai gratuit en cours
active     → Abonnement actif et à jour
past_due   → Paiement échoué, retry en cours
paused     → Suspendu
canceled   → Annulé

Upgrade / Downgrade avec proration :

// Changer de plan immédiatement avec facture de proration
await stripe.subscriptions.update(subscriptionId, {
  items: [{ id: subscriptionItemId, price: 'price_premium' }],
  proration_behavior: 'always_invoice',  // génère facture immédiate
})

Créer avec période d'essai :

await stripe.subscriptions.create({
  customer: customerId,
  items: [{ price: 'price_xxx' }],
  trial_period_days: 14,
})

Annuler (fin de période vs immédiat) :

// Fin de la période actuelle (recommandé UX)
await stripe.subscriptions.update(subscriptionId, {
  cancel_at_period_end: true,
})

// Immédiat
await stripe.subscriptions.cancel(subscriptionId)

📖 Docs : https://docs.stripe.com/billing/subscriptions/overview | https://docs.stripe.com/billing/subscriptions/upgrade-downgrade


/stripe portal — Customer Portal

Route app/api/billing/portal/route.ts :

import { stripe } from '@/adapters/payments/stripe'
import { auth } from '@/adapters/auth'
import { NextResponse } from 'next/server'

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

  // Récupérer le stripeCustomerId depuis votre DB
  const user = await db.users.findUnique({ where: { id: session.user.id } })
  if (!user?.stripeCustomerId) {
    return NextResponse.json({ error: 'No billing account' }, { status: 404 })
  }

  const portal = await stripe.billingPortal.sessions.create({
    customer: user.stripeCustomerId,
    return_url: `${process.env.NEXT_PUBLIC_APP_URL}/billing`,
  })

  return NextResponse.json({ data: { url: portal.url } })
}

Le Customer Portal gère automatiquement : changement de plan, annulation, mise à jour CB, historique des factures.

📖 Docs : https://docs.stripe.com/customer-management/integrate-customer-portal


Schema DB recommandé (Drizzle)

// Table subscriptions liée à organizations
export const subscriptions = pgTable('subscriptions', {
  id:                   uuid('id').defaultRandom().primaryKey(),
  organizationId:       uuid('organization_id').references(() => organizations.id),
  stripeCustomerId:     text('stripe_customer_id').unique(),
  stripeSubscriptionId: text('stripe_subscription_id').unique(),
  stripePriceId:        text('stripe_price_id'),
  stripeCurrentPeriodEnd: timestamp('stripe_current_period_end'),
  status:               text('status'),  // active | trialing | past_due | canceled
  plan:                 text('plan'),    // free | pro | enterprise
  createdAt:            timestamp('created_at').defaultNow(),
  updatedAt:            timestamp('updated_at').defaultNow(),
})

// Table pour l'idempotence des webhooks
export const stripeEvents = pgTable('stripe_events', {
  id:          text('id').primaryKey(),   // event.id de Stripe
  type:        text('type'),
  processedAt: timestamp('processed_at').defaultNow(),
})

Checklist pré-production

  • Clés sk_live_ + pk_live_ configurées dans Vercel (pas les sk_test_)
  • Webhook endpoint enregistré dans Stripe Dashboard → Developers → Webhooks
  • STRIPE_WEBHOOK_SECRET en prod différent du local (whsec_...)
  • RLS sur subscriptions table (par organization_id)
  • Test avec stripe trigger sur l'env staging avant prod
  • Customer Portal activé dans Stripe Dashboard → Billing → Customer Portal

📖 Docs officielles complètes : https://docs.stripe.com/billing/subscriptions/build-subscriptions

Gives 0 of the 12 instructions most pricing monetisation skills give

Counted across 366 of the 366 authors here whose files we hold, read 2026-08-06

  • verify webhook signaturesin 23 of 366, across 19 files
  • differentiate tiers using features, limits, or supportin 15 of 366, across 4 files
  • read product marketing context before asking questionsin 14 of 366, across 6 files
  • base price on perceived value, not costin 14 of 366, across 3 files
  • use Van Westendorp to find acceptable price rangein 14 of 366, across 3 files
  • use MaxDiff to identify highly valued featuresin 14 of 366, across 3 files
  • choose a value metric that scales with customer valuein 14 of 366, across 9 files
  • handle webhook events idempotentlyin 12 of 366, across 6 files
  • understand the upgrade context before recommendingin 11 of 366, across 4 files
  • align the pricing metric with delivered valuein 10 of 366, across 4 files
  • install stripe packagein 10 of 366, across 5 files
  • calculate unit economics metricsin 10 of 366, across 5 files

Said here and by no other author read

  • activate access when receiving invoice.paid
  • restrict access when receiving invoice.payment_failed
  • sync the database plan on subscription.updated
  • disable body parser for webhooks
  • return 500 to trigger stripe retry on failure
  • use live keys in production

Grouped from the skills themselves: near-identical wordings counted once, and counted by distinct author, so one author publishing three of these counts once.

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.