Token导航 LogoToken导航

Agent Skill · 研究检索

stripe-integration-expert(Stripe 集成 expert)

stripe-integration-expert 用于查找、检索和筛选相关信息,适合在 Codex、Claude、Cursor、Gemini CLI 中需要根据关键词、任务场景或来源线索快速定位候选结果时使用。可结合来源仓库、安装命令和原始 README 继续核验具体用法。安装前建议确认权限范围、维护状态,以及是否会触发联网、命令执行或文件读写。

stripe-integration-expert 是研究检索类 Agent Skill,可用宿主为 Codex、Claude、Cursor、Gemini CLI,通过GitHub 仓库获取,权限为需要联网;许可证信息还不明确,第三方扫描有提醒项,按安装量在研究检索类的 73,675 个 Skill 里排第 38,381。

总安装 1,567周安装 64GitHub Stars 103下载 502核验 2026-05-01

安装说明

本站只整理中文说明,不托管安装包,也不代用户安装。
GitHub
许可证
unknown

安装方式

通过对话安装

复制提示词发给支持本地命令或 Skills 的 AI 助手,先确认命令和权限,再让它执行。

请帮我安装这个 Agent Skill:stripe-integration-expert(Stripe 集成 expert)
来源仓库:https://github.com/borghei/claude-skills
仓库路径:skills/stripe-integration-expert
安装命令:
npx skills add https://github.com/borghei/claude-skills --skill stripe-integration-expert
安装前请先检查当前环境是否支持对应 CLI,并向我确认将要执行的命令、安装目录、联网范围和文件读写权限;确认后再执行。

命令行安装

复制命令到本机终端执行。该命令会通过 npx skills 从第三方来源获取 Skill;本站只展示命令,不托管安装包,也不自动执行。

skills.shnpx skills
npx skills add https://github.com/borghei/claude-skills --skill stripe-integration-expert

这个 Skill 是做什么的

研究检索类 Skill 主要解决「找资料、核对事实、把来源整理清楚」的问题,通常会让助手先检索再总结。

用于构建生产级 Stripe 计费集成,覆盖订阅生命周期管理与 webhook 处理。支持试用、按比例计费、Customer Portal 及 SCA/3D Secure 合规性实现。提供 Next.js、Express、Django 等主流框架下的实战模式与边缘案例处理方案。

需具备有效 Stripe API 凭证方可部署完整功能,仅本地模拟部分受限操作。stripe-integration-expert 属于研究检索类 Skill,可作为该场景下的辅助能力补充。

来源仓库内路径:skills/stripe-integration-expert

上手步骤

4 步
  1. 1

    确认自己的宿主工具

    公开资料标注支持 Codex、Claude、Cursor、Gemini CLI;先确认自己用的是其中之一,再按对应工具的 Skills 目录放置。

  2. 2

    拿到安装命令

    可以直接复制的命令是 npx skills add https://github.com/borghei/claude-skills --skill stripe-integrati…;命令里的占位符换成自己的路径或密钥再执行。

  3. 3

    安装后重启宿主

    命令执行完要完全退出宿主工具再打开,让新装的 Skill 被重新扫描到,否则列表里可能看不到。

  4. 4

    按最小权限试用

    它需要需要联网,先在测试目录或测试账号里跑,确认没问题再放到正式项目。

常见问题

6 条

stripe-integration-expert 怎么安装?

在终端执行 npx skills add https://github.com/borghei/claude-skills --skill stripe-integration-expert。装完完全退出宿主工具再打开,让它重新扫描本地 Skill。

stripe-integration-expert 支持哪些 AI 工具?

公开资料标注支持 Codex、Claude、Cursor、Gemini CLI。其他工具只要能读取本地 Skill 目录,一般也能手动放进去,但要自己确认触发方式是否一致。

stripe-integration-expert 需要什么权限?

权限级别是需要联网。该 Skill 可能需要联网访问来源站点、仓库或外部 API;具体网络访问范围需要结合源码和 README 复核。

stripe-integration-expert 是免费的吗?

公开资料没有标注明确许可证,收费和商用限制都无法确认。正式使用前请到来源仓库核对授权说明。

stripe-integration-expert 用的人多吗?

公开数据是累计安装 1,567 次,近一周安装 64 次,下载 502 次,GitHub 103 星。这些数字来自第三方目录统计,只能当作热度参考,不代表质量或安全性。

stripe-integration-expert 安全吗?

第三方扫描有提醒项,说明存在需要人工确认的行为。安装前请打开源码看清它读了什么、联网到哪里。

值得选的地方和要先想清楚的地方

值得选的地方

  • 同时适配 4 种工具,换客户端不用重新找替代品。

先想清楚的地方

  • 许可证没有明确标注,能不能商用、能不能再分发都不确定。
  • 第三方扫描存在提醒项或未通过项,需要人工逐条确认。
  • 第三方目录整理,版本更新和行为细节以来源仓库为准。

适不适合你

更适合

  • 已经在用 Codex,想直接扩展它能力的人。
  • 需要处理研究检索类任务、又不想每次都手写提示词的人。

不太适合

  • 需要明确授权才能用到公司项目里的场景。
  • 需要厂商级技术支持和 SLA 的生产系统。

研究检索类 Skill 对比

6 个同类
Skill总安装GitHub 星权限许可证扫描
find-skills

寻找技能 · Codex、Claude

31,199,81316,486操作浏览器MIT有提醒
self-improving-agent

自我改进 Agent · OpenClaw

16,464,0023,396接触敏感数据MIT-0通过
soultrace

灵魂追踪 · Codex、Claude

7,588,94412需要联网未明确有提醒
self-improving

自我完善 · OpenClaw

6,829,2841,046需要联网MIT-0通过
microsoft-foundry

微软代工厂 · Codex、Claude

6,614,144764调用外部服务未明确有提醒
stripe-integration-expert

Stripe 集成 expert · 本页

1,567103需要联网未明确有提醒

按安装量取同分类前几名;数字来自第三方目录统计,只作热度参考,不代表质量结论。

SKILL.md 原文

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

// 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

// 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)

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)

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)

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)

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.

// 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

// 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

// 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

// 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.

// 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

# 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)

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

PitfallConsequencePrevention
Trusting webhook event dataStale data, race conditionsAlways re-fetch from Stripe API in handlers
No idempotency on webhooksDouble-charges, duplicate recordsTrack processed event IDs in database
Missing metadata on checkoutCannot link subscription to userAlways pass userId in metadata
Proration surprisesUsers charged unexpected amountsAlways preview proration before upgrade
Not handling past_dueUsers lose access without warningImplement dunning emails on payment failure
Skipping trial abuse preventionUsers create multiple accounts for free trialsStore hasHadTrial: true, check on checkout
Customer Portal not configuredPortal returns blank pageEnable features in Stripe Dashboard first
Webhook endpoint not idempotentStripe retries cause duplicate processingIdempotency table with event ID dedup
Not pinning API versionBreaking changes on Stripe updatesPin apiVersion in client constructor
Ignoring trial_will_end eventUsers surprised when trial endsSend reminder email 3 days before

Related Skills

SkillUse When
ab-test-setupTesting pricing page variants and checkout flows
analytics-trackingTracking checkout and subscription conversion events
email-template-builderBuilding dunning and billing notification emails
api-design-reviewerReviewing your billing API endpoints

Troubleshooting

ProblemCauseSolution
Webhook returns 400 on all eventsWebhook signing secret mismatch between environmentsVerify STRIPE_WEBHOOK_SECRET matches the endpoint in Stripe Dashboard; use stripe listen output secret for local dev
Checkout session redirects to blank pagesuccess_url or cancel_url missing {CHECKOUT_SESSION_ID} template or pointing to wrong domainEnsure URLs use APP_URL env var and include the session ID template literal for retrieval
Subscription shows incomplete statusFirst payment requires 3D Secure but was never completedHandle checkout.session.async_payment_failed and send the customer a link to complete authentication
Proration invoice charges full price instead of differenceUsing create_prorations instead of always_invoice or not passing existing subscription item IDUse 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 cancellationConfirm the price uses recurring.usage_type: "metered" and the subscription is active before reporting
Customer Portal shows no optionsPortal configuration not enabled in Stripe DashboardNavigate to Stripe Dashboard > Settings > Billing > Customer Portal and enable subscription management features
Duplicate webhook processing despite idempotency tablemarkProcessed called before handler completes, then handler throws on retryMove 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

SkillIntegrationData Flow
api-design-reviewerReview billing API endpoints for REST conventions, error handling, and rate limitingBilling route definitions --> API review checklist --> validated endpoint contracts
database-schema-designerDesign and validate the Prisma schema for Stripe customer, subscription, and event tracking tablesSchema requirements --> normalized table design --> migration files
observability-designerInstrument webhook handlers and checkout flows with structured logging, metrics, and alertingWebhook events --> OpenTelemetry traces --> dashboard alerts on failure spikes
env-secrets-managerManage Stripe API keys, webhook secrets, and price IDs across dev/staging/productionSecret definitions --> encrypted vault storage --> runtime injection via env vars
ci-cd-pipeline-builderAutomate Stripe CLI webhook testing in CI and validate integration before deploymentTest triggers --> stripe listen in CI --> webhook handler assertions
runbook-generatorCreate operational runbooks for billing incidents: failed webhooks, mass payment failures, subscription reconciliationIncident scenarios --> step-by-step remediation --> escalation paths

平台分布

Codex

39.79%
按下载量换算200

Claude

28.72%
按下载量换算144

Cursor

17.55%
按下载量换算88

Gemini CLI

9.81%
按下载量换算49

安全审计

Gen Agent Trust Hub

通过

Socket

通过

Snyk

可疑

权限和风险

需要联网

该 Skill 可能需要联网访问来源站点、仓库或外部 API;具体网络访问范围需要结合源码和 README 复核。

安装前确认

本站仅展示第三方公开信息,不托管安装包,不提供自动安装或运行环境。安装前应自行审查源码、依赖和命令行为。来源安全扫描存在提醒或未通过项,安装前请自行复核。目前只有一个公开来源,建议同时核对源仓库。

继续浏览同类 Skills