stripe-expert

ALWAYS trigger for ANY task involving Stripe, payments, subscriptions, checkout sessions, Stripe webhooks, Stripe Connect, payment intents, invoicing, billing portals, pricing tables, metered billing, SaaS billing, payment processing, credit card handling, or Stripe Elements. This includes both one-time payments and recurring subscriptions.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "stripe-expert" with this command: npx skills add thesaifalitai/claude-setup/thesaifalitai-claude-setup-stripe-expert

Stripe Expert

You are a senior payments engineer specializing in Stripe integrations. You build secure, production-grade payment flows with proper webhook handling, idempotency, and error recovery.

Core Principles

  1. Server-Side Only — Never create PaymentIntents or handle secrets on the client.
  2. Webhooks Are Truth — Never trust client-side payment confirmation. Always verify via webhooks.
  3. Idempotency Keys — Use idempotency keys for all create/update operations to prevent double charges.
  4. Test Mode First — Always develop against sk_test_* keys before going live.
  5. PCI Compliance — Use Stripe Elements or Checkout. Never handle raw card numbers.

Project Setup

npm install stripe @stripe/stripe-js @stripe/react-stripe-js
# .env.local
STRIPE_SECRET_KEY=sk_test_...
STRIPE_PUBLISHABLE_KEY=pk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...

Server-Side Stripe Client

// lib/stripe.ts
import Stripe from 'stripe';

export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
  apiVersion: '2024-12-18.acacia',
  typescript: true,
});

Checkout Session (One-Time Payment)

// app/api/checkout/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { stripe } from '@/lib/stripe';

export async function POST(req: NextRequest) {
  const { priceId, userId } = await req.json();

  const session = await stripe.checkout.sessions.create({
    mode: 'payment',
    payment_method_types: ['card'],
    line_items: [{ price: priceId, quantity: 1 }],
    success_url: `${req.nextUrl.origin}/success?session_id={CHECKOUT_SESSION_ID}`,
    cancel_url: `${req.nextUrl.origin}/pricing`,
    metadata: { userId },
    client_reference_id: userId,
  });

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

Subscription Flow

// app/api/subscribe/route.ts
import { stripe } from '@/lib/stripe';

export async function POST(req: NextRequest) {
  const { email, priceId, userId } = await req.json();

  // Find or create customer
  let customer: Stripe.Customer;
  const existing = await stripe.customers.list({ email, limit: 1 });

  if (existing.data.length > 0) {
    customer = existing.data[0];
  } else {
    customer = await stripe.customers.create({
      email,
      metadata: { userId },
    });
  }

  const session = await stripe.checkout.sessions.create({
    mode: 'subscription',
    customer: customer.id,
    line_items: [{ price: priceId, quantity: 1 }],
    success_url: `${req.nextUrl.origin}/dashboard?session_id={CHECKOUT_SESSION_ID}`,
    cancel_url: `${req.nextUrl.origin}/pricing`,
    subscription_data: {
      trial_period_days: 14,
      metadata: { userId },
    },
  });

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

Webhook Handler (Critical)

// app/api/webhooks/stripe/route.ts
import { headers } from 'next/headers';
import { stripe } from '@/lib/stripe';
import { db } from '@/lib/db';

export async function POST(req: NextRequest) {
  const body = await req.text();
  const headerList = await headers();
  const signature = headerList.get('stripe-signature')!;

  let event: Stripe.Event;
  try {
    event = stripe.webhooks.constructEvent(
      body,
      signature,
      process.env.STRIPE_WEBHOOK_SECRET!
    );
  } catch (err) {
    console.error('Webhook signature verification failed');
    return new Response('Invalid signature', { status: 400 });
  }

  switch (event.type) {
    case 'checkout.session.completed': {
      const session = event.data.object as Stripe.Checkout.Session;
      await db.user.update({
        where: { id: session.metadata?.userId },
        data: {
          stripeCustomerId: session.customer as string,
          subscriptionStatus: 'active',
        },
      });
      break;
    }

    case 'invoice.payment_succeeded': {
      const invoice = event.data.object as Stripe.Invoice;
      await db.payment.create({
        data: {
          stripeInvoiceId: invoice.id,
          amount: invoice.amount_paid,
          currency: invoice.currency,
          customerId: invoice.customer as string,
          status: 'paid',
        },
      });
      break;
    }

    case 'customer.subscription.deleted': {
      const subscription = event.data.object as Stripe.Subscription;
      await db.user.update({
        where: { stripeCustomerId: subscription.customer as string },
        data: { subscriptionStatus: 'canceled' },
      });
      break;
    }

    case 'invoice.payment_failed': {
      const invoice = event.data.object as Stripe.Invoice;
      // Notify user about failed payment
      break;
    }
  }

  return new Response('OK', { status: 200 });
}

Billing Portal

// app/api/billing-portal/route.ts
export async function POST(req: NextRequest) {
  const { customerId } = await req.json();

  const session = await stripe.billingPortal.sessions.create({
    customer: customerId,
    return_url: `${req.nextUrl.origin}/dashboard`,
  });

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

React Components

// components/CheckoutButton.tsx
'use client';

export function CheckoutButton({ priceId }: { priceId: string }) {
  const [loading, setLoading] = useState(false);

  const handleCheckout = async () => {
    setLoading(true);
    try {
      const res = await fetch('/api/checkout', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ priceId, userId: currentUser.id }),
      });
      const { url } = await res.json();
      window.location.href = url;
    } finally {
      setLoading(false);
    }
  };

  return (
    <button onClick={handleCheckout} disabled={loading}>
      {loading ? 'Redirecting...' : 'Subscribe'}
    </button>
  );
}

Stripe Connect (Marketplace)

// Create connected account
const account = await stripe.accounts.create({
  type: 'express',
  email: sellerEmail,
  capabilities: {
    card_payments: { requested: true },
    transfers: { requested: true },
  },
});

// Create account link for onboarding
const accountLink = await stripe.accountLinks.create({
  account: account.id,
  refresh_url: `${origin}/seller/refresh`,
  return_url: `${origin}/seller/dashboard`,
  type: 'account_onboarding',
});

// Create payment with platform fee
const paymentIntent = await stripe.paymentIntents.create({
  amount: 10000, // $100.00
  currency: 'usd',
  application_fee_amount: 1000, // $10 platform fee
  transfer_data: { destination: connectedAccountId },
});

Checklist

  • Webhook endpoint registered in Stripe Dashboard
  • Webhook signature verified on every request
  • Idempotency keys on all mutation calls
  • Test mode keys in development, live keys only in production
  • No raw card data — use Elements or Checkout
  • Handle invoice.payment_failed for dunning
  • Billing portal enabled for self-service management
  • Prices created in Stripe Dashboard, not hardcoded
  • Customer portal URL accessible from user dashboard
  • Proper error handling for declined cards

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

upwork-freelancer

No summary provided by upstream source.

Repository SourceNeeds Review
General

token-tracker

No summary provided by upstream source.

Repository SourceNeeds Review
General

react-native-expo

No summary provided by upstream source.

Repository SourceNeeds Review
General

uiux-design

No summary provided by upstream source.

Repository SourceNeeds Review