关于 clerk-billing
面向使用 Clerk Billing 的开发者,说明订阅方案、功能权益、访问限制和账单 Webhook 的实现方式。内容包含用户与组织计费,以及通过 CLI 和配置 API 管理计费设置。
主要功能
- 01用户与组织计费方案
- 02定价表与订阅界面
- 03方案和功能权益检查
- 04按席位限制组织成员
- 05计费配置与 Webhook 处理
适用场景
- 01展示可用订阅方案并引导用户选择
- 02按功能或方案等级限制页面与 API
- 03为组织订阅配置席位上限
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ clerk/ skills --skill clerk-billing安装后在 Claude Code 中输入/clerk-billing确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
evals/evals.jsonreferences/b2b-patterns.mdb2c-patterns.mdbilling-components.mdbilling-webhooks.md详细介绍
计费
停止,前置条件。 必须先启用计费,
<PricingTable />、<CheckoutButton />、has({ plan })或has({ feature })才能正常使用。两种方式:(1)控制台 → 计费 → 设置,或(2)clerk enable billing(见下方“Agent 优先:以编程方式配置计费”)。启用后会自动创建默认的free_user/free_org计划。开发实例可以使用共享的 Clerk 开发网关(无需 Stripe 账户);生产环境仅在处理付款时需要 Stripe 账户。**注意:**计费 API 仍处于实验阶段。请固定
@clerk/nextjs和clerk-js软件包版本。支持的版本表请参阅clerk技能。
快速开始
-
启用计费,可通过控制台 → 计费 → 设置或
clerk enable billing(见“Agent 优先”部分)完成。跳过此步骤会在开发环境中抛出cannot_render_billing_disabled,并在生产环境中渲染空内容。 -
在对应的标签页中创建计划,前往控制台 → 计费 → 计划。共有两个标签页,各标签页的 slug 仅在该标签页内有效,创建后无法移动:
- 用户计划 →
<PricingTable />(默认for="user") - 组织计划 →
<PricingTable for="organization" />
选错标签页是
<PricingTable />内容为空的首要原因。计划保存在 Clerk 中;不会同步到 Stripe。 - 用户计划 →
-
在计划中添加功能,在控制台 → 计费 → 计划中打开相应计划,使用其“功能”部分。功能仅适用于各自的计划,并非全局设置。同一个 slug 可以关联到多个计划;如果当前计划包含该 slug,
has({ feature: 'export' })就会匹配。 -
渲染
<PricingTable />(B2B 场景请传入for="organization")。 -
使用
auth()中的has({ plan })或has({ feature })控制访问权限。 -
处理计费 Webhook,以响应订阅生命周期事件。
控制台快捷链接
| 操作 | URL |
|---|---|
| 启用计费 | https://dashboard.clerk.com/last-active?path=billing/settings |
| 创建 / 编辑计划 | https://dashboard.clerk.com/last-active?path=billing/plans |
| 成员资格模式(B2C + B2B 共存) | https://dashboard.clerk.com/last-active?path=organizations-settings |
| 编辑功能 | 计划 → 点击某个计划 → “功能”部分(无直接链接) |
Agent 优先:以编程方式配置计费
完整的计费配置(启用开关、计划、功能、计划与功能的关联)都可以通过 PLAPI 编辑,无需使用控制台。适用于 Agent 创建初始计划、在多个实例间复制配置,或对计费结构进行版本控制。
前置条件:项目已关联到 Clerk 应用(执行 clerk auth login 和 clerk link,参阅 clerk-cli)。计费仅适用于账户;与组织不同,未认领的应用无法启用计费,因此请先认领该应用。
通过 CLI 启用计费
clerk enable billing # both targets (default, auto-creates free_user + free_org plans)
clerk enable billing --for org # org only
clerk enable billing --for user # user only
拉取当前计费配置
clerk config pull --keys billing > billing.json
这会将关联实例的当前计费配置(开关 + 计划 + 功能)写入 billing.json。
编辑并应用
编辑 billing.json 以添加或移除计划或功能,然后预览差异并应用:
clerk config patch --file billing.json --dry-run
clerk config patch --file billing.json
传入 --instance prod 可将目标设为生产实例,而非开发实例。
原始 PATCH(完全控制)
如需在不使用配置文件的情况下,一次性更新计划或功能:
clerk api --platform PATCH /v1/platform/applications/<app_id>/instances/<ins_id>/config \
-d '{"billing":{"plans":[{"slug":"pro","name":"Pro","amount":2000,"currency":"usd","payer_type":"user","is_recurring":true}],"features":[{"slug":"export","name":"Export"}]}}'
注意事项
- 此功能用于处理计费配置(开关 + 计划 + 功能目录)。订阅生命周期(用户选择计划、结账、续订、取消)仍通过
<PricingTable />和计费 Webhook 处理;有关生命周期事件,请参阅clerk-webhooks技能。 - 通过 PLAPI 计费配置处理器,完全支持操作顶层
features映射以及计划与功能的关联(同步)。
你需要什么?
| 任务 | 参考 |
|---|---|
<PricingTable /> 属性、<CheckoutButton />、<Show> 计费模式 | references/billing-components.md |
B2C 模式(个人用户订阅、Membership optional 前置条件) | references/b2c-patterns.md |
| B2B 模式(组织订阅、席位限制套餐、管理员控制的计费 UI) | references/b2b-patterns.md |
| Webhook 事件目录、载荷结构、处理程序模板 | references/billing-webhooks.md |
参考资料
| 参考 | 说明 |
|---|---|
references/billing-components.md | <PricingTable /> 和订阅 UI |
references/b2c-patterns.md | B2C 订阅计费模式 |
references/b2b-patterns.md | 包含组织订阅和席位限制套餐的 B2B 计费 |
references/billing-webhooks.md | 订阅生命周期事件处理 |
文档
功能与套餐:何时使用哪一种
使用 has({ feature: 'slug' }) 控制特定功能的访问权限,例如导出、分析、API 访问、审计日志。
使用 has({ plan: 'slug' }) 控制套餐层级的访问权限,例如显示专业版仪表板、检查组织订阅级别、重定向免费用户。
| 场景 | 正确的检查方式 |
|---|---|
| 控制“导出 CSV”按钮的访问权限 | has({ feature: 'export' }) |
| 控制“分析”版块的访问权限 | has({ feature: 'analytics' }) |
| 控制整个 /dashboard/pro 的访问权限 | has({ plan: 'pro' }) |
| 检查组织是否订阅了团队套餐 | has({ plan: 'org:team' }) |
| 控制 SSO 配置的访问权限 | has({ feature: 'sso' }) |
当用户说“控制导出功能的访问权限”或“控制分析功能的访问权限”时,始终使用 has({ feature })。只有在控制的对象是套餐层级本身,而非套餐中的特定功能时,才使用 has({ plan })。
关键模式
Next.js 服务端示例遵循 Cache Components 规则(cacheComponents: true,即 create-next-app 的默认设置),因为在 <Suspense> 边界之外读取请求数据会导致 next build 失败。使用 auth() 渲染数据的页面会在 <Suspense> 内的异步子组件中读取数据。通过 redirect() 控制渲染的页面会在顶部进行检查,并在旁边添加 loading.tsx。请参阅 clerk-nextjs-patterns 技能以及在 Suspense 内读取 auth 数据。
1. 渲染定价表
使用单个组件向用户展示可用套餐:
import { PricingTable } from '@clerk/nextjs'
export default function PricingPage() {
return (
<main>
<h1>Choose a plan</h1>
<PricingTable />
</main>
)
}
<PricingTable /> 会自动渲染 Clerk Dashboard 中配置的所有套餐。选择套餐后会打开 Clerk 应用内结账抽屉。基本用法无需传入属性。对于 B2B,传入 for="organization" 以渲染组织级套餐,而非用户套餐。
2. 检查功能权益(服务端)
按单项功能控制访问权限;对于特定功能,这是首选方式:
import { auth } from '@clerk/nextjs/server'
import { Suspense } from 'react'
async function Analytics() {
const { has } = await auth()
const canViewAnalytics = has({ feature: 'analytics' })
const canExport = has({ feature: 'export' })
return (
<div>
{canViewAnalytics && <AnalyticsChart />}
{canExport && <ExportButton />}
</div>
)
}
export default function AnalyticsPage() {
return (
<Suspense>
<Analytics />
</Suspense>
)
}
在 Clerk Dashboard → Billing → Features 中配置功能,并将其分配给套餐。控制细粒度功能时,使用 has({ feature }) 而非 has({ plan });应检查功能,而非套餐。
3. 检查功能权益(客户端)
使用 useAuth() 在客户端控制功能访问权限。结合服务端检查以实现全面覆盖:
'use client'
import { useAuth } from '@clerk/nextjs'
export function FeatureGatedUI() {
const { has, isLoaded } = useAuth()
if (!isLoaded) return null
const canExport = has?.({ feature: 'export' })
const canAnalytics = has?.({ feature: 'analytics' })
return (
<div>
{canAnalytics && <AnalyticsSection />}
{canExport ? <ExportButton /> : <UpgradeToExport />}
</div>
)
}
服务端组件使用 auth(),客户端组件使用 useAuth()。两者都支持 has({ feature }) 和 has({ plan })。
4. 在服务端检查订阅套餐
按订阅套餐控制访问权限(用于套餐层级的控制,而非单项功能):
// app/pro/page.tsx
import { auth } from '@clerk/nextjs/server'
import { redirect } from 'next/navigation'
export default async function ProDashboard() {
const { has } = await auth()
if (!has({ plan: 'pro' })) {
redirect('/pricing')
}
return <ProFeatures />
}
在旁边添加 app/pro/loading.tsx,例如 export default function Loading() { return <p>Loading...</p> }。
5. 在客户端检查套餐
在客户端组件中使用 useAuth() Hook:
'use client'
import { useAuth } from '@clerk/nextjs'
export function UpgradePrompt() {
const { has } = useAuth()
if (has?.({ plan: 'pro' })) {
return null
}
return (
<div>
<p>Upgrade to Pro to access this feature</p>
<a href="/pricing">View plans</a>
</div>
)
}
6. 使用组织实现 B2B 按席位计费
组织套餐可以设置席位上限(成员数量上限),由 Clerk 在发送邀请时强制执行。在组织侧检查套餐时使用 org: slug 前缀(例如,has({ plan: 'org:team' })),避免授权条件含糊不清。使用 <PricingTable for="organization" /> 显示 B2B 定价页面,并使用 <OrganizationProfile /> 显示组织账户的账单界面。
有关分层套餐命名、席位上限约束、仅管理员可操作的账单管理以及 webhook 处理程序,请参阅 references/b2b-patterns.md。
7. 显示订阅状态
使用 has({ plan }) 检查特定套餐,或在客户端组件中使用 useSubscription() 获取完整的订阅详情。请勿直接从 sessionClaims 读取套餐信息,这不是受支持的方式。
服务端组件,检查特定套餐:
import { auth } from '@clerk/nextjs/server'
import { Suspense } from 'react'
async function CurrentPlan() {
const { has } = await auth()
const currentPlan = has({ plan: 'pro' })
? 'pro'
: has({ plan: 'starter' })
? 'starter'
: 'free'
return (
<div>
<h2>Current Plan</h2>
<p>You are on the {currentPlan} plan</p>
{currentPlan === 'free' && <a href="/pricing">Upgrade</a>}
</div>
)
}
export default function AccountPage() {
return (
<Suspense>
<CurrentPlan />
</Suspense>
)
}
客户端组件,通过 useSubscription() 获取完整的订阅详情:
'use client'
import { useSubscription } from '@clerk/nextjs/experimental'
export function SubscriptionDetails() {
const { data: subscription, isLoading } = useSubscription()
if (isLoading) return null
if (!subscription) return <a href="/pricing">Choose a plan</a>
return (
<div>
<p>Status: {subscription.status}</p>
{subscription.nextPayment && (
<p>Next payment: {subscription.nextPayment.date.toLocaleDateString()}</p>
)}
</div>
)
}
useSubscription()仅用于显示。对于授权检查(限制内容或路由的访问),始终使用has({ plan })或has({ feature })。
8. 按套餐保护 API 路由
使用 auth() 限制 API 路由的访问:
import { auth } from '@clerk/nextjs/server'
import { NextResponse } from 'next/server'
export async function GET() {
const { has } = await auth()
if (!has({ plan: 'pro' })) {
return NextResponse.json({ error: 'Pro plan required' }, { status: 403 })
}
return NextResponse.json({ data: 'premium data' })
}
9. 处理账单 webhook
Clerk 的事件名称与 Stripe 的事件名称不同。 Clerk 账单 webhook 使用点号分隔的驼峰命名,而非 Stripe 的下划线格式。
没有
subscription.canceled事件。取消操作会在条目级别触发subscriptionItem.canceled。
意图 Stripe 事件名称 Clerk 事件名称 创建订阅 customer.subscription.createdsubscription.created更新订阅 customer.subscription.updatedsubscription.updated订阅已生效 (无) subscription.active订阅逾期 (无) subscription.pastDue取消订阅条目 customer.subscription.deletedsubscriptionItem.canceled订阅条目逾期 invoice.payment_failedsubscriptionItem.pastDue更新订阅条目 (无) subscriptionItem.updated订阅条目已生效 (无) subscriptionItem.active订阅条目即将续订 (无) subscriptionItem.upcoming订阅条目已结束 (无) subscriptionItem.ended订阅条目已放弃 (无) subscriptionItem.abandoned订阅条目已过期 (无) subscriptionItem.expired订阅条目未完成 (无) subscriptionItem.incomplete免费试用即将结束 (无) subscriptionItem.freeTrialEnding创建付款尝试 (无) paymentAttempt.created更新付款尝试 (无) paymentAttempt.updated在
evt.type检查中始终使用 Clerk 的事件名称,切勿使用 Stripe 的事件名称。
Payload 结构。 Clerk 账单 webhook 的 payload 是嵌套结构。订阅实体位于
evt.data.payer下(字段:user_id?、organization_id?)。套餐信息位于每个条目的evt.data.items[i].plan.slug中。订阅 ID 就是evt.data.id。订阅条目没有指向订阅的subscription_id字段,因此在subscriptionItem.*处理程序中,应通过条目 ID(evt.data.id)识别记录,或按付款方与套餐查找记录。
用于说明此模式的最简处理程序(从 @clerk/nextjs/webhooks 导入,验证后根据 Clerk 事件名称分支处理):
import { verifyWebhook } from '@clerk/nextjs/webhooks'
import { NextRequest } from 'next/server'
import { db } from '@/lib/db'
export async function POST(req: NextRequest) {
let evt
try {
evt = await verifyWebhook(req)
} catch {
return new Response('Verification failed', { status: 400 })
}
if (evt.type === 'subscription.created') {
const { id, payer, items, status } = evt.data
const entityId = payer.organization_id ?? payer.user_id
const plan = items[0]?.plan?.slug
await db.subscriptions.upsert({
where: { subscriptionId: id },
create: { subscriptionId: id, entityId, plan, status },
update: { entityId, plan, status },
})
}
// Add more branches per the event catalog above (subscription.updated,
// subscriptionItem.canceled, subscriptionItem.pastDue, etc.)
return new Response('OK', { status: 200 })
}
完整模板涵盖全部 15 个事件、来自 @clerk/backend 的 TS 类型声明、proxy.ts 公共路由设置以及订阅状态值表,请参阅 references/billing-webhooks.md。
10. 升级 / 降级流程
在账单页面上显示 <PricingTable />,让用户在应用内管理订阅。对于已订阅的用户,该组件的显示方式会有所不同:它会显示当前套餐,并允许用户通过 Clerk 的应用内结账抽屉升级或取消订阅。有关根据套餐调整文案的完整账单页面,请参阅 references/billing-components.md。
计划和功能命名
计划 slug 和功能 slug 在 Clerk Dashboard → Billing 中定义。常见约定如下:
| 层级 | 计划 Slug | 功能示例 |
|---|---|---|
| Free | (无需检查计划) | 基本功能 |
| Starter | starter | analytics、api_access |
| Pro | pro | analytics、export、team |
| Enterprise | enterprise | 所有功能 + sso、audit_logs |
使用与您在 Dashboard 中定义的内容一致的小写 slug。
B2B 与 B2C 计费
| 场景 | 订阅者 | 计划检查 |
|---|---|---|
| B2C SaaS | 个人用户 | 在用户会话中使用 has({ plan: 'pro' }) |
| B2B SaaS | 组织 | 在组织会话中使用 has({ plan: 'org:team' }) |
| 席位数受限的 B2B | 组织 | 计划有席位上限;按计划定价,而非按成员定价,为更大的组织设置更高层级的计划 |
对于 B2B,请确保用户拥有有效的组织会话。has() 检查会评估当前活动实体(用户或组织)。
结账流程
Clerk 会通过 <PricingTable /> 和 <CheckoutButton /> 自动呈现自己的结账抽屉。计划和定价由 Clerk 管理。若要从服务器操作触发结账,请重定向到一个呈现 <PricingTable /> 的页面:
'use server'
import { redirect } from 'next/navigation'
export async function upgradeAction() {
redirect('/pricing')
}
错误特征(快速诊断)
如果遇到以下任何错误或症状,几乎总是需要调整 Dashboard 开关,而非修改代码。不要一开始就编辑组件。
| 错误 / 症状 | 根本原因 | 修复方法 |
|---|---|---|
Clerk: 🔒 The <PricingTable/> component cannot be rendered when billing is disabled.(代码:cannot_render_billing_disabled,仅开发环境) | 此实例未启用计费 | 在 dashboard.clerk.com → Billing → Settings 中启用计费,或运行 clerk enable billing。 |
<PricingTable /> 呈现为空 | 没有计划,或计划位于错误的标签页(User 与 Organization),或未启用计费 | 在对应的标签页中创建计划;B2B 请传入 for="organization";检查 Billing Settings |
| 用户无法在同时支持 B2C 和 B2B 的应用中订阅个人计划 | 成员资格必需模式(自 2025-08-22 起为默认模式)会禁用个人账户,已登录用户会被强制进入 choose-organization,无法进入个人订阅状态 | 如果需要同时支持个人订阅和组织订阅:Dashboard → Organizations settings → Membership optional |
| 找不到 Features 页面 | 功能按计划设置,而非全局设置 | Dashboard → Billing → Plans → 点击计划 → Features |
成功结账后,has({ plan: 'pro' }) 始终返回 false | 会话令牌尚未刷新,未包含新计划 | 使用 await clerk.session?.reload(),或通过导航强制创建新会话 |
尚未尝试订阅时,has({ plan: 'pro' }) 返回 false | 计划 slug 不匹配(区分大小写),或未启用计费,或生产环境中未连接支付网关 | 在 Dashboard → Billing → Plans 中核对 slug;确认 Billing → Settings 显示已启用且网关已连接 |
对拥有该角色的用户,has({ permission: 'org:x:y' }) 返回 false | 与该权限关联的 Feature 未包含在组织的当前有效计划中 | 在 Dashboard → Billing → Plans → Features 中将该 Feature 添加到计划 |
| Webhook 返回 401 或 404 / 签名验证失败 | CLERK_WEBHOOK_SIGNING_SECRET 不匹配,或 auth.protect() 覆盖了 webhook 路由 | 从 Dashboard → Webhooks 复制 Signing Secret。确保中间件或处理程序中没有任何内容对 /api/webhooks(.*) 调用 auth.protect() |
计费会限制权限
启用计费后,即使用户通过其角色获得了权限,如果与该权限关联的 Feature 未包含在组织的当前有效计划中,has({ permission: 'org:posts:edit' }) 也会返回 false。这是预期行为:计费会在功能层面限制权限。请务必确保所需的 Feature 已添加到 Dashboard → Billing → Plans → Features 中的计划。
另请参阅
clerk-setup- Clerk 初始安装clerk-orgs- B2B 组织(B2B 计费和席位上限方案所需)clerk-webhooks- Webhook 签名验证和路由