Stripe integration
Skill findscripter/everything-skills/10-platform/stripe-integration
类书式 AI Agent 技能大典 · 精选/中文化/互见成网的 500+ 开源技能,可作为 Claude Code 插件市场一键安装。A curated, cross-referenced encyclopedia of 500+ open-source agent skills.
npx -y skills add findscripter/everything-skills --skill stripe-integrationAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
2 things to look at
- no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
当为 Web 应用接入 Stripe 支付/订阅、调试 webhook 可靠性、迁移支付服务商或加用量计费时使用;做一套含 Checkout、订阅升降级按比例计费、幂等 webhook、客户门户与用量上报的生产级集成(Next.js/Express/Django);不适用于线下收款、非 Stripe 渠道或纯前端样式问题;触发词:Stripe、订阅、subscription、支付、checkout、webhook、按比例计费、proration、用量计费、customer portal、试用、past_due。
The file declares its own license as MIT. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.
SKILL.md
10.0 KB, as published. Nobody here has run it
何时使用
适用:
- 给 Web 应用首次接入订阅计费,或叠加用量/坐席计费。
- 实现套餐升级/降级,并需要按比例计费(proration)与额度抵扣。
- 排查 webhook 投递失败、重复处理、状态不同步等可靠性问题。
- 从其他支付服务商迁移计费模型。
不该用:
- 线下/对公转账、纯人工对账场景。
- 非 Stripe 渠道(支付宝、微信、PayPal 等)——本条命令与字段不通用。
- 仅是定价页样式、按钮文案等纯前端问题。
核心原则:DB 是 Stripe 状态的「缓存」,唯一可信源是 Stripe API;所有关键写库都应在 webhook 中、并以重新拉取的对象为准。
步骤
- 客户端初始化:用
STRIPE_SECRET_KEY创建单例stripe,固定apiVersion;把各套餐的 Price ID 收敛到一张PLANS表(按 monthly/yearly)。 - 建 Checkout 会话:先「取或建」Stripe Customer(在
metadata.userId写入业务用户 ID 并回写库),再创建mode:"subscription"的 Checkout Session,按需给未试用过的用户加trial_period_days。 - 落地幂等 Webhook:先
constructEvent验签 → 查stripeEvent幂等表 → 按事件类型分发 → 成功后才写入幂等表;处理失败返回 500 让 Stripe 重试。 - 升降级:升级
proration_behavior:"always_invoice"立即生效;降级proration_behavior:"none"周期末生效;确认前用 upcoming invoice 预览金额。 - 用量计费:对 metered 价格项调用
createUsageRecord上报增量。 - 自助管理:用
billingPortal.sessions.create跳转客户门户。 - 本地测试:用 Stripe CLI 转发 webhook 并
trigger触发事件,用测试卡号验证成功/失败路径。
指令
订阅状态机(建议库内 status 取值:trialing | active | past_due | canceled | cancel_pending | paused | unpaid):
FREE_TRIAL ──paid──► ACTIVE ──cancel──► CANCEL_PENDING ──period_end──► CANCELED
│ │ downgrade reactivate
│ ▼ │
│ DOWNGRADING ──period_end──► ACTIVE(lower) │
└──trial_end 无支付──► PAST_DUE ──失败3次──► CANCELED;支付成功──► ACTIVE
客户端单例:
// lib/stripe.ts
import Stripe from "stripe"
export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: "2024-04-10",
typescript: true,
appInfo: { name: "myapp", version: "1.0.0" },
})
export const PLANS = {
starter: { monthly: process.env.STRIPE_STARTER_MONTHLY_PRICE_ID!, yearly: process.env.STRIPE_STARTER_YEARLY_PRICE_ID! },
pro: { monthly: process.env.STRIPE_PRO_MONTHLY_PRICE_ID!, yearly: process.env.STRIPE_PRO_YEARLY_PRICE_ID! },
} as const
升降级 + proration 预览:
export async function changeSubscriptionPlan(subscriptionId: string, newPriceId: string, immediate = false) {
const sub = await stripe.subscriptions.retrieve(subscriptionId)
const item = sub.items.data[0]
return stripe.subscriptions.update(subscriptionId, {
items: [{ id: item.id, price: newPriceId }],
proration_behavior: immediate ? "always_invoice" : "none", // 升级立即出账 / 降级周期末
billing_cycle_anchor: "unchanged",
})
}
export async function previewProration(subscriptionId: string, newPriceId: string) {
const sub = await stripe.subscriptions.retrieve(subscriptionId)
const invoice = await stripe.invoices.retrieveUpcoming({
customer: sub.customer as string,
subscription: subscriptionId,
subscription_items: [{ id: sub.items.data[0].id, price: newPriceId }],
subscription_proration_date: Math.floor(Date.now() / 1000),
})
return { amountDue: invoice.amount_due, lineItems: invoice.lines.data }
}
用量上报与客户门户:
export async function reportUsage(subscriptionItemId: string, quantity: number) {
await stripe.subscriptionItems.createUsageRecord(subscriptionItemId, {
quantity, timestamp: Math.floor(Date.now() / 1000), action: "increment",
})
}
// 客户门户
const portal = await stripe.billingPortal.sessions.create({
customer: user.stripeCustomerId,
return_url: `${process.env.NEXT_PUBLIC_APP_URL}/settings/billing`,
})
本地测试(Stripe CLI):
stripe login
stripe listen --forward-to localhost:3000/api/webhooks/stripe
stripe trigger checkout.session.completed
stripe trigger invoice.payment_failed
# 测试卡:成功 4242 4242 4242 4242;需 3DS 4000 0025 0000 3155;余额不足 4000 0000 0000 9995
示例
Checkout 会话(Next.js App Router;注意源码中 customers.create 的 name 后漏了逗号且用了占位值,下方已修正):
// app/api/billing/checkout/route.ts
export async function POST(req: Request) {
const user = await getAuthUser()
if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
const { priceId } = await req.json()
let stripeCustomerId = user.stripeCustomerId
if (!stripeCustomerId) {
const customer = await stripe.customers.create({
email: user.email,
name: user.name, // 修正:源码此处漏逗号且写成占位字符串
metadata: { userId: user.id },
})
stripeCustomerId = customer.id
await db.user.update({ where: { id: user.id }, data: { stripeCustomerId } })
}
const session = await stripe.checkout.sessions.create({
customer: stripeCustomerId,
mode: "subscription",
line_items: [{ price: priceId, quantity: 1 }],
allow_promotion_codes: true,
subscription_data: {
trial_period_days: user.hasHadTrial ? undefined : 14, // 只给没试用过的人 14 天
metadata: { userId: user.id },
},
success_url: `${process.env.NEXT_PUBLIC_APP_URL}/dashboard?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}/pricing`,
metadata: { userId: user.id },
})
return NextResponse.json({ url: session.url })
}
幂等 Webhook 骨架(验签 → 幂等 → 分发 → 标记):
// app/api/webhooks/stripe/route.ts
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 {
return NextResponse.json({ error: "Invalid signature" }, { status: 400 })
}
if (await hasProcessedEvent(event.id)) return NextResponse.json({ received: true, skipped: true })
try {
switch (event.type) {
case "checkout.session.completed": await handleCheckoutCompleted(event.data.object as any); break
case "customer.subscription.created":
case "customer.subscription.updated": await handleSubscriptionUpdated(event.data.object as any); break
case "customer.subscription.deleted": await handleSubscriptionDeleted(event.data.object as any); break
case "invoice.payment_succeeded": await handleInvoicePaymentSucceeded(event.data.object as any); break
case "invoice.payment_failed": await handleInvoicePaymentFailed(event.data.object as any); break
}
await markEventProcessed(event.id, event.type)
return NextResponse.json({ received: true })
} catch (err) {
// 返回 500 触发 Stripe 重试,且不写幂等表
return NextResponse.json({ error: "Processing failed" }, { status: 500 })
}
}
payment_failed 中按 attempt_count 做催款:达到 3 次发「最终催缴」邮件并置 past_due,否则发「重试」提醒。功能门控可用 isSubscriptionActive:active/trialing 直接放行,past_due 在 stripeCurrentPeriodEnd 之前给宽限期。
注意事项
- Webhook 投递顺序无保证:写库前务必用事件里的 ID 重新
retrieve最新对象,不要只信 event payload。 - 重复处理:Stripe 在 500 时会重试,必须有幂等表(成功后才标记已处理)。
- 防试用滥用:转化后在库中置
hasHadTrial: true,下次不再发试用。 - proration 易出意外金额:升级前必用 upcoming invoice 预览并让用户确认。
- 客户门户需先在 Dashboard 的 Billing → Customer portal 里启用相应功能,否则跳转报错。
- Checkout 必须在
metadata带userId,否则无法把订阅关联回业务用户。 - 验签依赖原始请求体:用
req.text()拿 raw body,勿先 JSON 解析。
互见
- code-reviewer:审查支付/幂等逻辑与密钥处理。
- dependency-auditor:核查 stripe SDK 版本与依赖安全。
—— 本条采编自 alirezarezvani/claude-skills(MIT 许可)。