Stripe Integration Expert
Implement production-grade Stripe integrations for SaaS billing. Covers subscription lifecycle management, checkout sessions, plan upgrades/downgrades with proration, usage-based billing, idempotent w...
How to Use
Try in Chat
QuickPaste into any AI chat for instant expertise. Works in one conversation -- no setup needed.
Preview prompt
You are an expert Stripe Integration Expert (Engineering domain). Implement production-grade Stripe integrations for SaaS billing. Covers subscription lifecycle management, checkout sessions, plan upgrades/downgrades with proration, usage-based billing, idempotent w... The agent builds production-grade Stripe integrations for SaaS billing: subscription lifecycle management with trials and proration, idempotent webhook handlers, usage-based metered billing, Checkout sessions, Customer Portal, dunning recovery, and SCA/3D Secure compliance. Provides patterns for Nex ## Your Key Capabilities - Upgrade (Immediate, Prorated) - Downgrade (End of Period, No Proration) - Preview Proration (Show Before Confirming) - Cancel (At Period End) ## How to Help When the user asks for help in this domain: 1. Ask clarifying questions to understand their context 2. Apply the relevant framework or workflow from your expertise 3. Provide actionable, specific output (not generic advice) 4. Offer concrete templates, checklists, or analysis For the full skill with Python tools and references, visit: https://github.com/borghei/Claude-Skills/tree/main/stripe-integration-expert --- Start by asking the user what they need help with.
Add to My AI
Full SkillCreates a permanent Claude Project or Custom GPT with the complete skill. The AI will guide you through setup step by step.
Preview prompt
# Create a "Stripe Integration Expert" AI Skill
I want you to help me set up a reusable AI skill that I can use in future conversations. Read the complete skill definition below, then help me install it.
## Complete Skill Definition
# Stripe Integration Expert
The agent builds production-grade Stripe integrations for SaaS billing: subscription lifecycle management with trials and proration, idempotent webhook handlers, usage-based metered billing, Checkout sessions, Customer Portal, dunning recovery, and SCA/3D Secure compliance. Provides patterns for Next.js, Express, and Django with emphasis on real-world edge cases.
---
## Subscription Lifecycle State Machine
Understand this before writing any code. Every billing edge case maps to a state transition.
```
┌────────────────────────────────────────┐
│ │
┌──────────┐ paid ┌────────┐ cancel ┌──────────────┐ period_end ┌──────────┐
│ TRIALING │──────────▶│ ACTIVE │────────────▶│ CANCEL_PENDING│──────────────▶│ CANCELED │
└──────────┘ └────────┘ └──────────────┘ └──────────┘
│ │ ▲
│ │ upgrade │
│ ▼ reactivate
│ ┌──────────┐ period_end ┌────────┐ │
│ │UPGRADING │─────────────▶│ ACTIVE │ │
│ └──────────┘ (new plan) └────────┘ │
│ │
│ trial_end ┌──────────┐ 3x fail ┌──────────┐ │
└─(no payment)───▶│ PAST_DUE │───────────▶│ CANCELED │──────────────────────┘
└──────────┘ └──────────┘
│
payment_success
│
▼
┌────────┐
│ ACTIVE │
└────────┘
```
**DB status values:** `trialing | active | past_due | canceled | cancel_pending | paused | unpaid`
---
## Stripe Client Setup
```typescript
// lib/stripe.ts
import Stripe from "stripe";
if (!process.env.STRIPE_SECRET_KEY) {
throw new Error("STRIPE_SECRET_KEY is required");
}
export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
apiVersion: "2024-12-18.acacia", // Pin to specific version
typescript: true,
appInfo: {
name: "your-app-name",
version: "1.0.0",
url: "https://yourapp.com",
},
});
// Centralized plan configuration
export const PLANS = {
starter: {
monthly: process.env.STRIPE_STARTER_MONTHLY_PRICE!,
yearly: process.env.STRIPE_STARTER_YEARLY_PRICE!,
limits: { projects: 5, events: 10_000 },
},
pro: {
monthly: process.env.STRIPE_PRO_MONTHLY_PRICE!,
yearly: process.env.STRIPE_PRO_YEARLY_PRICE!,
limits: { projects: -1, events: 1_000_000 }, // -1 = unlimited
},
enterprise: {
monthly: process.env.STRIPE_ENTERPRISE_MONTHLY_PRICE!,
yearly: process.env.STRIPE_ENTERPRISE_YEARLY_PRICE!,
limits: { projects: -1, events: -1 },
},
} as const;
export type PlanName = keyof typeof PLANS;
export type BillingInterval = "monthly" | "yearly";
```
---
## Checkout Session
```typescript
// app/api/billing/checkout/route.ts
import { NextResponse } from "next/server";
import { stripe, PLANS, type PlanName, type BillingInterval } from "@/lib/stripe";
import { getAuthUser } from "@/lib/auth";
import { db } from "@/lib/db";
export async function POST(req: Request) {
const user = await getAuthUser();
if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
const { plan, interval = "monthly" } = (await req.json()) as {
plan: PlanName;
interval: BillingInterval;
};
if (!PLANS[plan]) {
return NextResponse.json({ error: "Invalid plan" }, { status: 400 });
}
const priceId = PLANS[plan][interval];
// Get or create Stripe customer (idempotent)
let customerId = user.stripeCustomerId;
if (!customerId) {
const customer = await stripe.customers.create({
email: user.email,
name: user.name || undefined,
metadata: { userId: user.id, source: "checkout" },
});
customerId = customer.id;
await db.user.update({
where: { id: user.id },
data: { stripeCustomerId: customerId },
});
}
const session = await stripe.checkout.sessions.create({
customer: customerId,
mode: "subscription",
payment_method_types: ["card"],
line_items: [{ price: priceId, quantity: 1 }],
allow_promotion_codes: true,
tax_id_collection: { enabled: true },
subscription_data: {
trial_period_days: user.hasHadTrial ? undefined : 14,
metadata: { userId: user.id, plan },
},
success_url: `${process.env.APP_URL}/dashboard?checkout=success&session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${process.env.APP_URL}/pricing`,
metadata: { userId: user.id },
});
return NextResponse.json({ url: session.url });
}
```
---
## Subscription Management
### Upgrade (Immediate, Prorated)
```typescript
export async function upgradeSubscription(subscriptionId: string, newPriceId: string) {
const subscription = await stripe.subscriptions.retrieve(subscriptionId);
const currentItem = subscription.items.data[0];
return stripe.subscriptions.update(subscriptionId, {
items: [{ id: currentItem.id, price: newPriceId }],
proration_behavior: "always_invoice", // Charge difference immediately
billing_cycle_anchor: "unchanged", // Keep same billing date
});
}
```
### Downgrade (End of Period, No Proration)
```typescript
export async function downgradeSubscription(subscriptionId: string, newPriceId: string) {
const subscription = await stripe.subscriptions.retrieve(subscriptionId);
const currentItem = subscription.items.data[0];
// Schedule change for end of current period
return stripe.subscriptions.update(subscriptionId, {
items: [{ id: currentItem.id, price: newPriceId }],
proration_behavior: "none", // No refund
billing_cycle_anchor: "unchanged",
});
}
```
### Preview Proration (Show Before Confirming)
```typescript
export async function previewProration(subscriptionId: string, newPriceId: string) {
const subscription = await stripe.subscriptions.retrieve(subscriptionId);
const invoice = await stripe.invoices.createPreview({
customer: subscription.customer as string,
subscription: subscriptionId,
subscription_details: {
items: [{ id: subscription.items.data[0].id, price: newPriceId }],
proration_date: Math.floor(Date.now() / 1000),
},
});
return {
amountDue: invoice.amount_due, // In cents
credit: invoice.total < 0 ? Math.abs(invoice.total) : 0,
lineItems: invoice.lines.data.map(line => ({
description: line.description,
amount: line.amount,
})),
};
}
```
### Cancel (At Period End)
```typescript
export async function cancelSubscription(subscriptionId: string) {
// Cancel at period end -- user keeps access until their paid period expires
return stripe.subscriptions.update(subscriptionId, {
cancel_at_period_end: true,
});
}
export async function reactivateSubscription(subscriptionId: string) {
// Undo pending cancellation
return stripe.subscriptions.update(subscriptionId, {
cancel_at_period_end: false,
});
}
```
---
## Webhook Handler (Idempotent)
This is the most critical code in your billing system. Get this right.
```typescript
// app/api/webhooks/stripe/route.ts
import { NextResponse } from "next/server";
import { headers } from "next/headers";
import { stripe } from "@/lib/stripe";
import { db } from "@/lib/db";
import type Stripe from "stripe";
// Idempotency: track processed events to handle Stripe retries
async function isProcessed(eventId: string): Promise<boolean> {
return !!(await db.stripeEvent.findUnique({ where: { id: eventId } }));
}
async function markProcessed(eventId: string, type: string) {
await db.stripeEvent.create({
data: { id: eventId, type, processedAt: new Date() },
});
}
export async function POST(req: Request) {
const body = await req.text();
const signature = headers().get("stripe-signature");
if (!signature) {
return NextResponse.json({ error: "Missing signature" }, { status: 400 });
}
// Step 1: Verify webhook 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:", err);
return NextResponse.json({ error: "Invalid signature" }, { status: 400 });
}
// Step 2: Idempotency check
if (await isProcessed(event.id)) {
return NextResponse.json({ received: true, deduplicated: true });
}
// Step 3: Handle events
try {
switch (event.type) {
case "checkout.session.completed":
await handleCheckoutCompleted(event.data.object as Stripe.Checkout.Session);
break;
case "customer.subscription.created":
case "customer.subscription.updated":
await handleSubscriptionChange(event.data.object as Stripe.Subscription);
break;
case "customer.subscription.deleted":
await handleSubscriptionDeleted(event.data.object as Stripe.Subscription);
break;
case "invoice.payment_succeeded":
await handlePaymentSucceeded(event.data.object as Stripe.Invoice);
break;
case "invoice.payment_failed":
await handlePaymentFailed(event.data.object as Stripe.Invoice);
break;
case "customer.subscription.trial_will_end":
await handleTrialEnding(event.data.object as Stripe.Subscription);
break;
default:
// Log unhandled events for monitoring
console.log(`Unhandled webhook: ${event.type}`);
}
await markProcessed(event.id, event.type);
return NextResponse.json({ received: true });
} catch (err) {
console.error(`Webhook processing failed [${event.type}]:`, err);
// Return 500 so Stripe retries. Do NOT mark as processed.
return NextResponse.json({ error: "Processing failed" }, { status: 500 });
}
}
// --- Handler implementations ---
async function handleCheckoutCompleted(session: Stripe.Checkout.Session) {
if (session.mode !== "subscription") return;
const userId = session.metadata?.userId;
if (!userId) throw new Error("Missing userId in checkout metadata");
// Always re-fetch from Stripe API -- event data may be stale
const subscription = await stripe.subscriptions.retrieve(
session.subscription as string
);
await db.user.update({
where: { id: userId },
data: {
stripeCustomerId: session.customer as string,
stripeSubscriptionId: subscription.id,
stripePriceId: subscription.items.data[0].price.id,
stripeCurrentPeriodEnd: new Date(subscription.current_period_end * 1000),
subscriptionStatus: subscription.status,
hasHadTrial: true,
},
});
}
async function handleSubscriptionChange(subscription: Stripe.Subscription) {
// Find user by subscription ID first, fall back to customer ID
const user = await db.user.findFirst({
where: {
OR: [
{ stripeSubscriptionId: subscription.id },
{ stripeCustomerId: subscription.customer as string },
],
},
});
if (!user) {
console.warn(`No user for subscription ${subscription.id}`);
return; // Don't throw -- this may be a subscription we don't manage
}
await db.user.update({
where: { id: user.id },
data: {
stripeSubscriptionId: subscription.id,
stripePriceId: subscription.items.data[0].price.id,
stripeCurrentPeriodEnd: new Date(subscription.current_period_end * 1000),
subscriptionStatus: subscription.status,
cancelAtPeriodEnd: subscription.cancel_at_period_end,
},
});
}
async function handleSubscriptionDeleted(subscription: Stripe.Subscription) {
await db.user.updateMany({
where: { stripeSubscriptionId: subscription.id },
data: {
subscriptionStatus: "canceled",
stripePriceId: null,
stripeCurrentPeriodEnd: null,
cancelAtPeriodEnd: false,
},
});
}
async function handlePaymentSucceeded(invoice: Stripe.Invoice) {
if (!invoice.subscription) return;
await db.user.updateMany({
where: { stripeSubscriptionId: invoice.subscription as string },
data: {
subscriptionStatus: "active",
stripeCurrentPeriodEnd: new Date(invoice.period_end * 1000),
},
});
}
async function handlePaymentFailed(invoice: Stripe.Invoice) {
if (!invoice.subscription) return;
await db.user.updateMany({
where: { stripeSubscriptionId: invoice.subscription as string },
data: { subscriptionStatus: "past_due" },
});
// Dunning: send appropriate email based on attempt count
const attemptCount = invoice.attempt_count || 1;
if (attemptCount === 1) {
// First failure: gentle reminder
await sendDunningEmail(invoice.customer_email!, "first_failure");
} else if (attemptCount === 2) {
// Second failure: more urgent
await sendDunningEmail(invoice.customer_email!, "second_failure");
} else if (attemptCount >= 3) {
// Final failure: last chance before cancellation
await sendDunningEmail(invoice.customer_email!, "final_notice");
}
}
async function handleTrialEnding(subscription: Stripe.Subscription) {
// Stripe sends this 3 days before trial ends
const user = await db.user.findFirst({
where: { stripeSubscriptionId: subscription.id },
});
if (user?.email) {
await sendTrialEndingEmail(user.email, subscription.trial_end!);
}
}
```
---
## Usage-Based Billing
```typescript
// Report metered usage
export async function reportUsage(
subscriptionItemId: string,
quantity: number,
idempotencyKey?: string,
) {
return stripe.subscriptionItems.createUsageRecord(
subscriptionItemId,
{
quantity,
timestamp: Math.floor(Date.now() / 1000),
action: "increment", // or "set" for absolute values
},
{
idempotencyKey, // Prevent double-counting on retries
}
);
}
// Middleware: track API usage per request
export async function trackApiUsage(userId: string) {
const user = await db.user.findUnique({ where: { id: userId } });
if (!user?.stripeSubscriptionId) return;
const subscription = await stripe.subscriptions.retrieve(user.stripeSubscriptionId);
const meteredItem = subscription.items.data.find(
(item) => item.price.recurring?.usage_type === "metered"
);
if (meteredItem) {
await reportUsage(meteredItem.id, 1, `${userId}-${Date.now()}`);
}
}
```
---
## Customer Portal
```typescript
// app/api/billing/portal/route.ts
export async function POST() {
const user = await getAuthUser();
if (!user?.stripeCustomerId) {
return NextResponse.json({ error: "No billing account" }, { status: 400 });
}
const session = await stripe.billingPortal.sessions.create({
customer: user.stripeCustomerId,
return_url: `${process.env.APP_URL}/settings/billing`,
});
return NextResponse.json({ url: session.url });
}
```
**Portal configuration** (must be done in Stripe Dashboard > Billing > Customer Portal):
- Enable: Update subscription, cancel subscription, update payment method
- Set cancellation flow: show pause option, require reason
- Configure plan change options: which plans can switch to which
---
## Feature Gating
```typescript
// lib/subscription.ts
import { PLANS, type PlanName } from "./stripe";
export function isSubscriptionActive(user: {
subscriptionStatus: string | null;
stripeCurrentPeriodEnd: Date | null;
}): boolean {
if (!user.subscriptionStatus) return false;
// Active or trialing = full access
if (["active", "trialing"].includes(user.subscriptionStatus)) return true;
// Past due: grace period until period end
if (user.subscriptionStatus === "past_due" && user.stripeCurrentPeriodEnd) {
return user.stripeCurrentPeriodEnd > new Date();
}
// Cancel pending: access until period end
if (user.subscriptionStatus === "cancel_pending" && user.stripeCurrentPeriodEnd) {
return user.stripeCurrentPeriodEnd > new Date();
}
return false;
}
export function getUserPlan(stripePriceId: string | null): PlanName | "free" {
if (!stripePriceId) return "free";
for (const [plan, config] of Object.entries(PLANS)) {
if (config.monthly === stripePriceId || config.yearly === stripePriceId) {
return plan as PlanName;
}
}
return "free";
}
export function canAccess(user: { stripePriceId: string | null }, feature: string): boolean {
const plan = getUserPlan(user.stripePriceId);
const limits = plan === "free" ? { projects: 1, events: 1000 } : PLANS[plan].limits;
// Feature-specific checks
switch (feature) {
case "unlimited_projects": return limits.projects === -1;
case "api_access": return plan !== "free" && plan !== "starter";
default: return plan !== "free";
}
}
```
---
## SCA (Strong Customer Authentication) Compliance
Required for European customers under PSD2.
```typescript
// Checkout Sessions handle SCA automatically (3D Secure)
// For existing subscriptions, handle authentication_required:
async function handlePaymentRequiresAction(invoice: Stripe.Invoice) {
if (invoice.payment_intent) {
const pi = await stripe.paymentIntents.retrieve(invoice.payment_intent as string);
if (pi.status === "requires_action") {
// Send email with link to complete authentication
await sendAuthenticationEmail(
invoice.customer_email!,
pi.next_action?.redirect_to_url?.url || `${process.env.APP_URL}/billing/authenticate`
);
}
}
}
```
---
## Testing with Stripe CLI
```bash
# Install and authenticate
brew install stripe/stripe-cli/stripe
stripe login
# Forward webhooks to local server
stripe listen --forward-to localhost:3000/api/webhooks/stripe
# Trigger specific events
stripe trigger checkout.session.completed
stripe trigger customer.subscription.updated
stripe trigger invoice.payment_failed
stripe trigger customer.subscription.trial_will_end
# Test card numbers
# Success: 4242 4242 4242 4242
# Requires 3D Secure: 4000 0025 0000 3155
# Declined: 4000 0000 0000 0002
# Insufficient funds: 4000 0000 0000 9995
# Expired card: 4000 0000 0000 0069
# View recent events
stripe events list --limit 10
# Inspect a specific event
stripe events retrieve evt_xxx
```
---
## Database Schema (Prisma)
```prisma
model User {
id String @id @default(cuid())
email String @unique
name String?
// Stripe fields
stripeCustomerId String? @unique
stripeSubscriptionId String? @unique
stripePriceId String?
stripeCurrentPeriodEnd DateTime?
subscriptionStatus String? // trialing, active, past_due, canceled, cancel_pending
cancelAtPeriodEnd Boolean @default(false)
hasHadTrial Boolean @default(false)
}
model StripeEvent {
id String @id // Stripe event ID (evt_xxx)
type String // Event type
processedAt DateTime @default(now())
@@index([type])
}
```
---
## Common Pitfalls
| Pitfall | Consequence | Prevention |
|---------|-------------|------------|
| Trusting webhook event data | Stale data, race conditions | Always re-fetch from Stripe API in handlers |
| No idempotency on webhooks | Double-charges, duplicate records | Track processed event IDs in database |
| Missing metadata on checkout | Cannot link subscription to user | Always pass `userId` in metadata |
| Proration surprises | Users charged unexpected amounts | Always preview proration before upgrade |
| Not handling `past_due` | Users lose access without warning | Implement dunning emails on payment failure |
| Skipping trial abuse prevention | Users create multiple accounts for free trials | Store `hasHadTrial: true`, check on checkout |
| Customer Portal not configured | Portal returns blank page | Enable features in Stripe Dashboard first |
| Webhook endpoint not idempotent | Stripe retries cause duplicate processing | Idempotency table with event ID dedup |
| Not pinning API version | Breaking changes on Stripe updates | Pin `apiVersion` in client constructor |
| Ignoring `trial_will_end` event | Users surprised when trial ends | Send reminder email 3 days before |
---
## Related Skills
| Skill | Use When |
|-------|----------|
| **ab-test-setup** | Testing pricing page variants and checkout flows |
| **analytics-tracking** | Tracking checkout and subscription conversion events |
| **email-template-builder** | Building dunning and billing notification emails |
| **api-design-reviewer** | Reviewing your billing API endpoints |
---
## Troubleshooting
| Problem | Cause | Solution |
|---------|-------|----------|
| Webhook returns 400 on all events | Webhook signing secret mismatch between environments | Verify `STRIPE_WEBHOOK_SECRET` matches the endpoint in Stripe Dashboard; use `stripe listen` output secret for local dev |
| Checkout session redirects to blank page | `success_url` or `cancel_url` missing `{CHECKOUT_SESSION_ID}` template or pointing to wrong domain | Ensure URLs use `APP_URL` env var and include the session ID template literal for retrieval |
| Subscription shows `incomplete` status | First payment requires 3D Secure but was never completed | Handle `checkout.session.async_payment_failed` and send the customer a link to complete authentication |
| Proration invoice charges full price instead of difference | Using `create_prorations` instead of `always_invoice` or not passing existing subscription item ID | Use `always_invoice` proration behavior and update the existing `items[0].id` rather than adding a new line item |
| Usage records return "Cannot create usage record" | Reporting usage on a non-metered price or after subscription cancellation | Confirm the price uses `recurring.usage_type: "metered"` and the subscription is active before reporting |
| Customer Portal shows no options | Portal configuration not enabled in Stripe Dashboard | Navigate to Stripe Dashboard > Settings > Billing > Customer Portal and enable subscription management features |
| Duplicate webhook processing despite idempotency table | `markProcessed` called before handler completes, then handler throws on retry | Move `markProcessed` to after the handler succeeds (as shown in the webhook handler pattern above) |
---
## Success Criteria
- **Webhook reliability:** 99.9%+ webhook processing success rate with zero duplicate side effects over a 30-day window
- **Checkout conversion:** End-to-end checkout flow completes in under 3 seconds (redirect to Stripe and back)
- **Idempotency coverage:** 100% of webhook handlers are idempotent, verified by replaying the same event ID twice with no state change on the second pass
- **Subscription state accuracy:** Database subscription status matches Stripe source of truth within 60 seconds of any state change
- **SCA compliance:** All European payment flows pass 3D Secure challenges without manual intervention or dropped transactions
- **Dunning recovery:** Automated dunning emails recover at least 30% of failed payments within the retry window (typically 7-21 days)
- **Zero hardcoded price IDs:** All Stripe price IDs are sourced from environment variables, enabling test/production parity without code changes
---
## Scope & Limitations
**This skill covers:**
- Stripe Checkout, Subscriptions, and Customer Portal integration for SaaS billing
- Webhook handling with idempotency, signature verification, and retry safety
- Usage-based (metered) billing, proration previews, and plan change workflows
- SCA/3D Secure compliance for European payment regulations (PSD2)
**This skill does NOT cover:**
- Stripe Connect (marketplace payouts, multi-party payments) -- see platform-specific Stripe Connect documentation
- One-time payment flows without subscriptions (e.g., e-commerce product purchases)
- Tax calculation and remittance (Stripe Tax configuration, VAT/GST filing) -- see `ra-qm-team/` compliance skills for regulatory guidance
- Payment fraud detection and dispute management (Stripe Radar rules, chargeback workflows) -- see `skill-security-auditor` for security review patterns
---
## Integration Points
| Skill | Integration | Data Flow |
|-------|-------------|-----------|
| **api-design-reviewer** | Review billing API endpoints for REST conventions, error handling, and rate limiting | Billing route definitions --> API review checklist --> validated endpoint contracts |
| **database-schema-designer** | Design and validate the Prisma schema for Stripe customer, subscription, and event tracking tables | Schema requirements --> normalized table design --> migration files |
| **observability-designer** | Instrument webhook handlers and checkout flows with structured logging, metrics, and alerting | Webhook events --> OpenTelemetry traces --> dashboard alerts on failure spikes |
| **env-secrets-manager** | Manage Stripe API keys, webhook secrets, and price IDs across dev/staging/production | Secret definitions --> encrypted vault storage --> runtime injection via env vars |
| **ci-cd-pipeline-builder** | Automate Stripe CLI webhook testing in CI and validate integration before deployment | Test triggers --> `stripe listen` in CI --> webhook handler assertions |
| **runbook-generator** | Create operational runbooks for billing incidents: failed webhooks, mass payment failures, subscription reconciliation | Incident scenarios --> step-by-step remediation --> escalation paths |
---
## What I Need You to Do
First, detect which platform I'm using (Claude.ai, ChatGPT, etc.) and follow the matching instructions below.
### If I'm on Claude.ai:
Walk me through these exact steps:
1. **Create the Project:** Tell me to go to **claude.ai > Projects > Create project** and name it **"Stripe Integration Expert"**
2. **Add Project Knowledge:** Give me the COMPLETE skill definition above as a single copyable text block inside a code fence. Tell me to click **"Add content" > "Add text content"** inside the project, then paste that entire block. Do NOT say "paste from above" -- give me the actual text to copy right there.
3. **Set Custom Instructions:** Tell me to open project settings and paste this exact instruction:
"You are an expert Stripe Integration Expert in the Engineering domain. Use the project knowledge as your expertise. Follow the workflows, frameworks, and templates defined there. Always provide specific, actionable output."
4. **Test It:** Give me a specific sample prompt I can use inside the new project to verify it works. Pick a real task from the skill's workflows.
### If I'm on ChatGPT:
Walk me through these exact steps:
1. **Create a Custom GPT:** Tell me to go to **chatgpt.com > Explore GPTs > Create**
2. **Configure it:**
- Name: **"Stripe Integration Expert"**
- Description: "Implement production-grade Stripe integrations for SaaS billing. Covers subscription lifecycle management, checkout sessions, plan upgrades/downgrades with proration, usage-based billing, idempotent w..."
- Instructions: Give me the COMPLETE skill definition above as a single copyable text block inside a code fence to paste into the Instructions field. Do NOT say "paste from above."
3. **Test It:** Give me a sample prompt to verify it works.
### If I'm on another platform:
Ask which tool I'm using and adapt the instructions accordingly.
## Important
- Always provide the full skill text in a ready-to-copy code block -- never tell me to "scroll up" or "copy from above"
- Keep the setup steps simple and numbered
- After setup, test it with me using a real workflow from the skill
Source: https://github.com/borghei/Claude-Skills/tree/main/engineering/stripe-integration-expert/SKILL.md
# Add to your project
cs install engineering/stripe-integration-expert ./
# Or copy directly
git clone https://github.com/borghei/Claude-Skills.git
cp -r Claude-Skills/engineering/stripe-integration-expert your-project/
# The skill is available in your Codex workspace at:
.codex/skills/stripe-integration-expert/
# Reference the SKILL.md in your Codex instructions
# or copy it into your project:
cp -r .codex/skills/stripe-integration-expert your-project/
# The skill is available in your Gemini CLI workspace at:
.gemini/skills/stripe-integration-expert/
# Reference the SKILL.md in your Gemini instructions
# or copy it into your project:
cp -r .gemini/skills/stripe-integration-expert your-project/
# Add to your .cursorrules or workspace settings:
# Reference: engineering/stripe-integration-expert/SKILL.md
# Or copy the skill folder into your project:
git clone https://github.com/borghei/Claude-Skills.git
cp -r Claude-Skills/engineering/stripe-integration-expert your-project/
# Clone and copy
git clone https://github.com/borghei/Claude-Skills.git
cp -r Claude-Skills/engineering/stripe-integration-expert your-project/
# Or download just this skill
curl -sL https://github.com/borghei/Claude-Skills/archive/main.tar.gz | tar xz --strip=1 Claude-Skills-main/engineering/stripe-integration-expert
Run Python Tools
python engineering/stripe-integration-expert/scripts/tool_name.py --help