stripe

Stripe Integration Helper

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" with this command: npx skills add andrehfp/tinyplate/andrehfp-tinyplate-stripe

Stripe Integration Helper

Assist with Stripe payment gateway integration for SaaS applications.

Quick Reference

Installation

bun add stripe @stripe/stripe-js

Environment Variables

Server-side (secret)

STRIPE_SECRET_KEY="sk_live_..." STRIPE_WEBHOOK_SECRET="whsec_..."

Client-side (publishable)

NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY="pk_live_..."

App URL for callbacks

NEXT_PUBLIC_APP_URL="https://your-app.com"

SDK Initialization

Server-side:

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

export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, { apiVersion: "2025-01-27.acacia", typescript: true, });

Client-side:

// lib/stripe-client.ts import { loadStripe } from "@stripe/stripe-js";

export const stripePromise = loadStripe( process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY! );

Common Tasks

  1. Create Checkout Session

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

export async function POST(request: NextRequest) { const { userId } = await auth(); if (!userId) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); }

const { priceId } = await request.json();

const session = await stripe.checkout.sessions.create({ mode: "subscription", payment_method_types: ["card"], line_items: [{ price: priceId, quantity: 1 }], success_url: ${process.env.NEXT_PUBLIC_APP_URL}/billing/success?session_id={CHECKOUT_SESSION_ID}, cancel_url: ${process.env.NEXT_PUBLIC_APP_URL}/pricing, metadata: { userId }, customer_email: user.email, // Optional: pre-fill email });

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

  1. Create Customer Portal Session

// app/api/billing/portal/route.ts export async function POST(request: NextRequest) { const { userId } = await auth(); if (!userId) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); }

// Get Stripe customer ID from your database const user = await db.query.users.findFirst({ where: eq(users.id, userId), });

if (!user?.stripeCustomerId) { return NextResponse.json({ error: "No subscription" }, { status: 400 }); }

const session = await stripe.billingPortal.sessions.create({ customer: user.stripeCustomerId, return_url: ${process.env.NEXT_PUBLIC_APP_URL}/dashboard, });

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

  1. Webhook Handler

// app/api/webhooks/stripe/route.ts import { NextRequest, NextResponse } from "next/server"; import { stripe } from "@/lib/stripe"; import Stripe from "stripe";

export async function POST(request: NextRequest) { const body = await request.text(); const signature = request.headers.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 NextResponse.json({ error: "Invalid signature" }, { status: 400 }); }

switch (event.type) { case "checkout.session.completed": { const session = event.data.object as Stripe.Checkout.Session; await handleCheckoutComplete(session); break; } case "customer.subscription.updated": { const subscription = event.data.object as Stripe.Subscription; await handleSubscriptionUpdate(subscription); break; } case "customer.subscription.deleted": { const subscription = event.data.object as Stripe.Subscription; await handleSubscriptionCancelled(subscription); break; } case "invoice.payment_failed": { const invoice = event.data.object as Stripe.Invoice; await handlePaymentFailed(invoice); break; } }

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

Webhook Events

Event When to Handle

checkout.session.completed

User completes checkout

customer.subscription.created

New subscription starts

customer.subscription.updated

Plan change, renewal

customer.subscription.deleted

Subscription cancelled

invoice.payment_succeeded

Successful payment

invoice.payment_failed

Failed payment attempt

customer.updated

Customer info changed

Subscription Status Values

Status Description

active

Subscription is current

past_due

Payment failed, retrying

canceled

Subscription ended

unpaid

All retry attempts failed

trialing

In trial period

incomplete

First payment pending

Database Schema

Users Table (add Stripe fields)

stripeCustomerId TEXT UNIQUE stripeSubscriptionId TEXT stripePriceId TEXT stripeCurrentPeriodEnd TIMESTAMP

Subscription Sync Pattern

async function syncSubscription( userId: string, subscription: Stripe.Subscription ) { await db .update(users) .set({ stripeSubscriptionId: subscription.id, stripePriceId: subscription.items.data[0].price.id, stripeCurrentPeriodEnd: new Date(subscription.current_period_end * 1000), }) .where(eq(users.id, userId)); }

Feature Gating

async function checkFeatureAccess(userId: string): Promise<boolean> { const user = await db.query.users.findFirst({ where: eq(users.id, userId), });

if (!user?.stripeSubscriptionId) return false;

// Check if subscription is still valid const now = new Date(); return user.stripeCurrentPeriodEnd > now; }

Testing

Test Card Numbers

Card Scenario

4242424242424242

Successful payment

4000000000000002

Card declined

4000002500003155

Requires 3D Secure

4000000000009995

Insufficient funds

Stripe CLI for Local Webhooks

Install Stripe CLI

brew install stripe/stripe-cli/stripe

Login

stripe login

Forward webhooks to local

stripe listen --forward-to localhost:3000/api/webhooks/stripe

Trigger test events

stripe trigger checkout.session.completed

Pricing Page Pattern

// Get prices from Stripe const prices = await stripe.prices.list({ active: true, expand: ["data.product"], });

// Display in component {prices.data.map((price) => ( <PriceCard key={price.id} name={(price.product as Stripe.Product).name} price={price.unit_amount! / 100} interval={price.recurring?.interval} priceId={price.id} /> ))}

Security Best Practices

  • Never expose secret key - Use STRIPE_SECRET_KEY only server-side

  • Verify webhook signatures - Always use stripe.webhooks.constructEvent

  • Idempotency - Store event IDs to prevent duplicate processing

  • Raw body for webhooks - Don't parse JSON before verification

  • Use metadata - Store userId in checkout session metadata

Common Issues

Issue Solution

Webhook signature invalid Use raw body, not parsed JSON

Customer not found Create customer before checkout

Subscription not syncing Check webhook event registration

Test cards failing Ensure using test mode keys

Portal not loading Verify customer has active subscription

Useful Commands

List products

stripe products list

List prices

stripe prices list

Get subscription

stripe subscriptions retrieve sub_xxx

Cancel subscription

stripe subscriptions cancel sub_xxx

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

seo-technical

No summary provided by upstream source.

Repository SourceNeeds Review
General

posthog

No summary provided by upstream source.

Repository SourceNeeds Review
General

marketing-copy

No summary provided by upstream source.

Repository SourceNeeds Review
General

abacatepay

No summary provided by upstream source.

Repository SourceNeeds Review