Skip to main content
Business & GrowthPayment Services261 lines

Stripe

Accept payments and manage subscriptions with Stripe. Use this skill when the

Quick Summary34 lines
You are a payments specialist who integrates Stripe into projects. You understand
the Stripe API surface — Checkout, Payment Intents, Subscriptions, Invoices,
Customer Portal, webhooks — and how to build reliable, PCI-compliant payment flows.

## Key Points

- Always verify webhooks with `stripe.webhooks.constructEvent`
- Use metadata on all objects to link Stripe records to your database
- Use Checkout for fastest integration — custom UI later if needed
- Use idempotency keys on all mutating API calls
- Store the Stripe customer ID in your user database
- Use `cancel_at_period_end: true` instead of immediate cancellation
- Test with Stripe CLI: `stripe listen --forward-to localhost:3000/api/webhooks/stripe`
- Use Stripe Tax for automatic tax calculation
- Granting access on client-side success redirect without webhook confirmation
- Not storing Stripe customer IDs — makes customer management impossible
- Not handling `invoice.payment_failed` — users lose access silently
- Using test keys in production or vice versa

## Quick Example

```bash
npm install stripe
```

```typescript
import Stripe from 'stripe';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
  apiVersion: '2024-12-18.acacia',
});
```
skilldb get payment-services-skills/StripeFull skill: 261 lines
Paste into your CLAUDE.md or agent config

Stripe Payment Integration

You are a payments specialist who integrates Stripe into projects. You understand the Stripe API surface — Checkout, Payment Intents, Subscriptions, Invoices, Customer Portal, webhooks — and how to build reliable, PCI-compliant payment flows.

Core Philosophy

Webhooks are the source of truth

Never trust client-side payment confirmation. Always verify payment state through webhooks. The checkout.session.completed or invoice.paid event is when you grant access — not when the user returns to your success URL.

Checkout for speed, Elements for control

Stripe Checkout is a hosted payment page that handles all UI, validation, and compliance. Use it when you want to ship fast. Stripe Elements gives you embeddable UI components for custom payment forms. Use it when design control matters.

Idempotent everything

Stripe supports idempotency keys on all mutating API calls. Use them. Webhook events can be delivered multiple times. Your handlers must be idempotent.

Setup

Install

npm install stripe

Initialize

import Stripe from 'stripe';

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

Key Techniques

Checkout Session (fastest integration)

const session = await stripe.checkout.sessions.create({
  mode: 'subscription', // or 'payment' for one-time
  line_items: [{
    price: 'price_xxx', // Price ID from Stripe dashboard
    quantity: 1,
  }],
  success_url: 'https://yourdomain.com/success?session_id={CHECKOUT_SESSION_ID}',
  cancel_url: 'https://yourdomain.com/pricing',
  customer_email: user.email,
  metadata: { userId: user.id },
  subscription_data: {
    trial_period_days: 14,
    metadata: { userId: user.id },
  },
});

// Redirect user to session.url

Payment Intent (custom form with Elements)

// Server: create payment intent
const paymentIntent = await stripe.paymentIntents.create({
  amount: 2900, // $29.00 in cents
  currency: 'usd',
  customer: customerId,
  metadata: { userId: user.id, plan: 'pro' },
  automatic_payment_methods: { enabled: true },
});

// Client: confirm with Stripe.js
const { error } = await stripe.confirmPayment({
  elements,
  confirmParams: {
    return_url: 'https://yourdomain.com/success',
  },
});

Subscriptions

// Create subscription directly
const subscription = await stripe.subscriptions.create({
  customer: customerId,
  items: [{ price: 'price_xxx' }],
  trial_period_days: 14,
  payment_behavior: 'default_incomplete',
  expand: ['latest_invoice.payment_intent'],
  metadata: { userId: user.id },
});

// Update subscription (change plan)
await stripe.subscriptions.update(subscriptionId, {
  items: [{
    id: subscription.items.data[0].id,
    price: 'price_new_xxx',
  }],
  proration_behavior: 'create_prorations',
});

// Cancel subscription
await stripe.subscriptions.update(subscriptionId, {
  cancel_at_period_end: true, // Cancel at end of billing period
});

Customer management

// Create customer
const customer = await stripe.customers.create({
  email: user.email,
  name: user.name,
  metadata: { userId: user.id },
});

// Attach payment method
await stripe.paymentMethods.attach(paymentMethodId, {
  customer: customer.id,
});

// Set default payment method
await stripe.customers.update(customer.id, {
  invoice_settings: { default_payment_method: paymentMethodId },
});

Customer billing portal

const portalSession = await stripe.billingPortal.sessions.create({
  customer: customerId,
  return_url: 'https://yourdomain.com/account',
});
// Redirect to portalSession.url

Metered billing

// Report usage for metered subscriptions
await stripe.subscriptionItems.createUsageRecord(subscriptionItemId, {
  quantity: 150, // API calls this period
  timestamp: Math.floor(Date.now() / 1000),
  action: 'set', // 'set' replaces, 'increment' adds
});

Invoices

// List invoices for a customer
const invoices = await stripe.invoices.list({
  customer: customerId,
  limit: 10,
});

// Create one-off invoice
const invoice = await stripe.invoices.create({
  customer: customerId,
  auto_advance: true,
});

await stripe.invoiceItems.create({
  customer: customerId,
  invoice: invoice.id,
  amount: 5000,
  currency: 'usd',
  description: 'Custom skill development',
});

await stripe.invoices.finalizeInvoice(invoice.id);

Webhook Processing

Critical webhooks to handle:

EventAction
checkout.session.completedGrant access, create user record
invoice.paidRenew access, send receipt
invoice.payment_failedWarn user, retry or restrict access
customer.subscription.updatedUpdate plan in database
customer.subscription.deletedRevoke access
customer.subscription.trial_will_endSend trial ending email (3 days before)
import Stripe from 'stripe';

const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET;

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

  let event: Stripe.Event;
  try {
    event = stripe.webhooks.constructEvent(body, sig, endpointSecret);
  } catch (err) {
    return new Response('Invalid signature', { status: 400 });
  }

  switch (event.type) {
    case 'checkout.session.completed': {
      const session = event.data.object as Stripe.Checkout.Session;
      const userId = session.metadata?.userId;
      await grantAccess(userId, session.subscription as string);
      break;
    }
    case 'invoice.paid': {
      const invoice = event.data.object as Stripe.Invoice;
      await renewAccess(invoice.subscription as string);
      await sendReceipt(invoice);
      break;
    }
    case 'invoice.payment_failed': {
      const invoice = event.data.object as Stripe.Invoice;
      await notifyPaymentFailed(invoice);
      break;
    }
    case 'customer.subscription.deleted': {
      const sub = event.data.object as Stripe.Subscription;
      await revokeAccess(sub.metadata?.userId);
      break;
    }
  }

  return new Response('OK');
}

Best Practices

  • Always verify webhooks with stripe.webhooks.constructEvent
  • Use metadata on all objects to link Stripe records to your database
  • Use Checkout for fastest integration — custom UI later if needed
  • Use idempotency keys on all mutating API calls
  • Store the Stripe customer ID in your user database
  • Use cancel_at_period_end: true instead of immediate cancellation
  • Test with Stripe CLI: stripe listen --forward-to localhost:3000/api/webhooks/stripe
  • Use Stripe Tax for automatic tax calculation

Anti-Patterns

  • Granting access on client-side success redirect without webhook confirmation
  • Not storing Stripe customer IDs — makes customer management impossible
  • Not handling invoice.payment_failed — users lose access silently
  • Using test keys in production or vice versa
  • Not using idempotency keys — causes duplicate charges
  • Building custom billing portal instead of using Stripe's
  • Hardcoding prices instead of using Stripe Price objects

Install this skill directly: skilldb add payment-services-skills

Get CLI access →