Integrates Stripe payments including checkout, subscriptions, webhooks, and customer management. Use when accepting payments, setting up subscriptions, handling webhooks, or managing billing.
Handles Stripe payment processing, subscriptions, and webhooks for checkout sessions, customer management, and billing operations.
/plugin marketplace add mgd34msu/goodvibes-plugin/plugin install goodvibes@goodvibes-marketThis skill inherits all available tools. When active, it can use any tool Claude has access to.
Payment processing platform for online businesses with powerful APIs.
Install:
npm install stripe @stripe/stripe-js
Environment variables:
# .env.local
STRIPE_SECRET_KEY=sk_test_...
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
// lib/stripe.ts
import Stripe from 'stripe';
export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: '2023-10-16',
typescript: true,
});
// app/api/checkout/route.ts
import { NextResponse } from 'next/server';
import { stripe } from '@/lib/stripe';
export async function POST(req: Request) {
const { priceId, quantity = 1 } = await req.json();
try {
const session = await stripe.checkout.sessions.create({
mode: 'payment', // or 'subscription'
payment_method_types: ['card'],
line_items: [
{
price: priceId,
quantity,
},
],
success_url: `${process.env.NEXT_PUBLIC_APP_URL}/success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}/canceled`,
});
return NextResponse.json({ url: session.url });
} catch (error) {
return NextResponse.json(
{ error: 'Failed to create checkout session' },
{ status: 500 }
);
}
}
'use client';
import { loadStripe } from '@stripe/stripe-js';
const stripePromise = loadStripe(
process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!
);
export function CheckoutButton({ priceId }: { priceId: string }) {
const handleCheckout = async () => {
const response = await fetch('/api/checkout', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ priceId }),
});
const { url } = await response.json();
if (url) {
window.location.href = url;
}
};
return (
<button onClick={handleCheckout}>
Buy Now
</button>
);
}
// Create checkout with custom product
const session = await stripe.checkout.sessions.create({
mode: 'payment',
line_items: [
{
price_data: {
currency: 'usd',
product_data: {
name: 'Custom Product',
description: 'A great product',
images: ['https://example.com/image.png'],
},
unit_amount: 2000, // $20.00 in cents
},
quantity: 1,
},
],
success_url: `${process.env.NEXT_PUBLIC_APP_URL}/success`,
cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}/canceled`,
});
const session = await stripe.checkout.sessions.create({
mode: 'subscription',
customer: customerId, // Optional: existing customer
line_items: [
{
price: 'price_monthly_plan',
quantity: 1,
},
],
success_url: `${process.env.NEXT_PUBLIC_APP_URL}/dashboard?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}/pricing`,
subscription_data: {
trial_period_days: 14,
metadata: {
userId: 'user_123',
},
},
});
// Cancel subscription
await stripe.subscriptions.update(subscriptionId, {
cancel_at_period_end: true,
});
// Cancel immediately
await stripe.subscriptions.cancel(subscriptionId);
// Update subscription
await stripe.subscriptions.update(subscriptionId, {
items: [
{
id: subscriptionItemId,
price: 'price_new_plan',
},
],
proration_behavior: 'create_prorations',
});
// app/api/portal/route.ts
import { stripe } from '@/lib/stripe';
import { NextResponse } from 'next/server';
export async function POST(req: Request) {
const { customerId } = await req.json();
const session = await stripe.billingPortal.sessions.create({
customer: customerId,
return_url: `${process.env.NEXT_PUBLIC_APP_URL}/dashboard`,
});
return NextResponse.json({ url: session.url });
}
// app/api/payment-intent/route.ts
import { stripe } from '@/lib/stripe';
import { NextResponse } from 'next/server';
export async function POST(req: Request) {
const { amount, currency = 'usd' } = await req.json();
try {
const paymentIntent = await stripe.paymentIntents.create({
amount, // In cents
currency,
automatic_payment_methods: {
enabled: true,
},
});
return NextResponse.json({
clientSecret: paymentIntent.client_secret,
});
} catch (error) {
return NextResponse.json(
{ error: 'Failed to create payment intent' },
{ status: 500 }
);
}
}
'use client';
import { useState } from 'react';
import { loadStripe } from '@stripe/stripe-js';
import {
Elements,
PaymentElement,
useStripe,
useElements,
} from '@stripe/react-stripe-js';
const stripePromise = loadStripe(
process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!
);
function CheckoutForm() {
const stripe = useStripe();
const elements = useElements();
const [error, setError] = useState<string | null>(null);
const [processing, setProcessing] = useState(false);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!stripe || !elements) return;
setProcessing(true);
setError(null);
const { error: submitError } = await stripe.confirmPayment({
elements,
confirmParams: {
return_url: `${window.location.origin}/success`,
},
});
if (submitError) {
setError(submitError.message ?? 'Payment failed');
setProcessing(false);
}
};
return (
<form onSubmit={handleSubmit}>
<PaymentElement />
{error && <p className="text-red-500">{error}</p>}
<button
type="submit"
disabled={!stripe || processing}
className="mt-4 w-full bg-blue-600 text-white py-2 rounded"
>
{processing ? 'Processing...' : 'Pay'}
</button>
</form>
);
}
export function PaymentForm({ clientSecret }: { clientSecret: string }) {
return (
<Elements
stripe={stripePromise}
options={{
clientSecret,
appearance: {
theme: 'stripe',
variables: {
colorPrimary: '#3b82f6',
},
},
}}
>
<CheckoutForm />
</Elements>
);
}
// app/api/webhooks/stripe/route.ts
import { stripe } from '@/lib/stripe';
import { headers } from 'next/headers';
import { NextResponse } from 'next/server';
import Stripe from 'stripe';
export async function POST(req: Request) {
const body = await req.text();
const signature = 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 }
);
}
try {
switch (event.type) {
case 'checkout.session.completed': {
const session = event.data.object as Stripe.Checkout.Session;
await handleCheckoutComplete(session);
break;
}
case 'customer.subscription.created': {
const subscription = event.data.object as Stripe.Subscription;
await handleSubscriptionCreated(subscription);
break;
}
case 'customer.subscription.updated': {
const subscription = event.data.object as Stripe.Subscription;
await handleSubscriptionUpdated(subscription);
break;
}
case 'customer.subscription.deleted': {
const subscription = event.data.object as Stripe.Subscription;
await handleSubscriptionDeleted(subscription);
break;
}
case 'invoice.paid': {
const invoice = event.data.object as Stripe.Invoice;
await handleInvoicePaid(invoice);
break;
}
case 'invoice.payment_failed': {
const invoice = event.data.object as Stripe.Invoice;
await handlePaymentFailed(invoice);
break;
}
default:
console.log(`Unhandled event type: ${event.type}`);
}
return NextResponse.json({ received: true });
} catch (error) {
console.error('Webhook handler error:', error);
return NextResponse.json(
{ error: 'Webhook handler failed' },
{ status: 500 }
);
}
}
// Handler functions
async function handleCheckoutComplete(session: Stripe.Checkout.Session) {
const customerId = session.customer as string;
const customerEmail = session.customer_email;
// Create or update user in database
await db.user.upsert({
where: { email: customerEmail! },
update: { stripeCustomerId: customerId },
create: {
email: customerEmail!,
stripeCustomerId: customerId,
},
});
}
async function handleSubscriptionCreated(subscription: Stripe.Subscription) {
await db.subscription.create({
data: {
stripeSubscriptionId: subscription.id,
stripeCustomerId: subscription.customer as string,
status: subscription.status,
priceId: subscription.items.data[0].price.id,
currentPeriodEnd: new Date(subscription.current_period_end * 1000),
},
});
}
async function handleSubscriptionUpdated(subscription: Stripe.Subscription) {
await db.subscription.update({
where: { stripeSubscriptionId: subscription.id },
data: {
status: subscription.status,
priceId: subscription.items.data[0].price.id,
currentPeriodEnd: new Date(subscription.current_period_end * 1000),
},
});
}
async function handleSubscriptionDeleted(subscription: Stripe.Subscription) {
await db.subscription.update({
where: { stripeSubscriptionId: subscription.id },
data: { status: 'canceled' },
});
}
# Install Stripe CLI
brew install stripe/stripe-cli/stripe
# Login
stripe login
# Forward webhooks
stripe listen --forward-to localhost:3000/api/webhooks/stripe
const customer = await stripe.customers.create({
email: 'user@example.com',
name: 'John Doe',
metadata: {
userId: 'user_123',
},
});
const customer = await stripe.customers.retrieve(customerId);
// With subscriptions
const customer = await stripe.customers.retrieve(customerId, {
expand: ['subscriptions'],
});
await stripe.customers.update(customerId, {
email: 'newemail@example.com',
metadata: {
plan: 'pro',
},
});
const product = await stripe.products.create({
name: 'Pro Plan',
description: 'Full access to all features',
images: ['https://example.com/pro.png'],
metadata: {
features: 'unlimited,priority_support,api_access',
},
});
// One-time price
const price = await stripe.prices.create({
product: product.id,
unit_amount: 2000, // $20.00
currency: 'usd',
});
// Recurring price
const monthlyPrice = await stripe.prices.create({
product: product.id,
unit_amount: 1000, // $10.00
currency: 'usd',
recurring: {
interval: 'month',
},
});
// Usage-based price
const usagePrice = await stripe.prices.create({
product: product.id,
currency: 'usd',
recurring: {
interval: 'month',
usage_type: 'metered',
},
billing_scheme: 'per_unit',
unit_amount: 10, // $0.10 per unit
});
const products = await stripe.products.list({
active: true,
expand: ['data.default_price'],
});
// lib/stripe-helpers.ts
export function formatPrice(amount: number, currency: string = 'usd') {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: currency.toUpperCase(),
}).format(amount / 100);
}
export function getSubscriptionStatus(status: string) {
const statusMap: Record<string, { label: string; color: string }> = {
active: { label: 'Active', color: 'green' },
trialing: { label: 'Trial', color: 'blue' },
past_due: { label: 'Past Due', color: 'yellow' },
canceled: { label: 'Canceled', color: 'red' },
unpaid: { label: 'Unpaid', color: 'red' },
};
return statusMap[status] || { label: status, color: 'gray' };
}
export async function getOrCreateCustomer(userId: string, email: string) {
// Check if customer exists in database
const user = await db.user.findUnique({ where: { id: userId } });
if (user?.stripeCustomerId) {
return user.stripeCustomerId;
}
// Create new Stripe customer
const customer = await stripe.customers.create({
email,
metadata: { userId },
});
// Save to database
await db.user.update({
where: { id: userId },
data: { stripeCustomerId: customer.id },
});
return customer.id;
}
stripe listen locally| Mistake | Fix |
|---|---|
| Missing webhook verification | Use constructEvent() |
| Trusting client amounts | Calculate on server |
| No error handling | Wrap in try/catch |
| Hardcoded prices | Use price IDs from Stripe |
| Missing idempotency | Check webhook event IDs |
This skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.