Loading...
Loading...
Compare original and translation side by side
STOP, Dashboard-only prerequisite. Billing must be enabled from the Clerk Dashboard before any,<PricingTable />,<CheckoutButton />, orhas({ plan })usage works. The Clerk CLI and Backend API do not expose a toggle for this today, the only path is dashboard.clerk.com → your app → Billing → Settings. Dev instances can use the shared Clerk development gateway (no Stripe account needed); production requires a Stripe account for payment processing only.has({ feature })Note: Billing APIs are still experimental. Pin yourand@clerk/nextjspackage versions. Seeclerk-jsskill for the supported version table.clerk
⚠️ 注意:仅支持通过仪表盘配置。在使用任何、<PricingTable />、<CheckoutButton />或has({ plan })之前,必须先在Clerk仪表盘中启用Billing功能。目前Clerk CLI和后端API不提供启用开关,唯一路径是dashboard.clerk.com → 你的应用 → Billing → Settings。开发环境可使用共享的Clerk开发网关(无需Stripe账户);生产环境仅需绑定Stripe账户用于支付处理。has({ feature })提示:Billing API仍处于实验阶段。请固定和@clerk/nextjs的包版本。查看clerk-js技能文档获取支持版本列表。clerk
cannot_render_billing_disabled<PricingTable />for="user"<PricingTable for="organization" /><PricingTable />has({ feature: 'export' })<PricingTable />for="organization"has({ plan })has({ feature })auth()cannot_render_billing_disabled<PricingTable />for="user"<PricingTable for="organization" /><PricingTable />has({ feature: 'export' })<PricingTable />for="organization"auth()has({ plan })has({ feature })| Action | URL |
|---|---|
| Enable Billing | |
| Create / edit plans | |
| Membership mode (B2C + B2B coexistence) | |
| Edit features | Plans → click a plan → Features section (no direct URL) |
| 操作 | 链接 |
|---|---|
| 启用Billing功能 | |
| 创建/编辑计划 | |
| 会员模式(B2C+B2B共存) | |
| 编辑功能 | 计划 → 点击对应计划 → 功能模块(无直接链接) |
| Task | Reference |
|---|---|
| references/billing-components.md |
B2C patterns (individual user subscriptions, | references/b2c-patterns.md |
| B2B patterns (org subscriptions, seat-limit plans, admin-gated billing UI) | references/b2b-patterns.md |
| Webhook event catalog, payload shapes, handler templates | references/billing-webhooks.md |
| 任务 | 参考链接 |
|---|---|
| references/billing-components.md |
| B2C模式(个人用户订阅,需先设置“可选会员”) | references/b2c-patterns.md |
| B2B模式(组织订阅、席位限制计划、管理员专属账单UI) | references/b2b-patterns.md |
| Webhook事件目录、负载结构、处理器模板 | references/billing-webhooks.md |
| Reference | Description |
|---|---|
| |
| B2C subscription billing patterns |
| B2B billing with organization subscriptions and seat-limit plans |
| Subscription lifecycle event handling |
| 参考文档 | 描述 |
|---|---|
| |
| B2C订阅账单实现模式 |
| 基于组织订阅和席位限制计划的B2B账单方案 |
| 订阅生命周期事件处理指南 |
has({ feature: 'slug' })has({ plan: 'slug' })| Scenario | Correct check |
|---|---|
| Gate the "Export CSV" button | |
| Gate the "Analytics" section | |
| Gate all of /dashboard/pro | |
| Check if org has team subscription | |
| Gate SSO configuration | |
has({ feature })has({ plan })has({ feature: 'slug' })has({ plan: 'slug' })| 场景 | 正确校验方式 |
|---|---|
| 管控“导出CSV”按钮 | |
| 管控“分析”板块 | |
| 管控整个/dashboard/pro页面 | |
| 检查组织是否有团队订阅 | |
| 管控SSO配置 | |
has({ feature })has({ plan })import { PricingTable } from '@clerk/nextjs'
export default function PricingPage() {
return (
<main>
<h1>Choose a plan</h1>
<PricingTable />
</main>
)
}<PricingTable />for="organization"import { PricingTable } from '@clerk/nextjs'
export default function PricingPage() {
return (
<main>
<h1>选择你的套餐</h1>
<PricingTable />
</main>
)
}<PricingTable />for="organization"import { auth } from '@clerk/nextjs/server'
export default async function AnalyticsPage() {
const { has } = await auth()
const canViewAnalytics = has({ feature: 'analytics' })
const canExport = has({ feature: 'export' })
return (
<div>
{canViewAnalytics && <AnalyticsChart />}
{canExport && <ExportButton />}
</div>
)
}has({ feature })has({ plan })import { auth } from '@clerk/nextjs/server'
export default async function AnalyticsPage() {
const { has } = await auth()
const canViewAnalytics = has({ feature: 'analytics' })
const canExport = has({ feature: 'export' })
return (
<div>
{canViewAnalytics && <AnalyticsChart />}
{canExport && <ExportButton />}
</div>
)
}has({ feature })has({ plan })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 })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 })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 />
}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 />
}useAuth()'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>
)
}useAuth()'use client'
import { useAuth } from '@clerk/nextjs'
export function UpgradePrompt() {
const { has } = useAuth()
if (has?.({ plan: 'pro' })) {
return null
}
return (
<div>
<p>升级到专业版以访问此功能</p>
<a href="/pricing">查看套餐</a>
</div>
)
}org:has({ plan: 'org:team' })<PricingTable for="organization" /><OrganizationProfile />references/b2b-patterns.mdorg:has({ plan: 'org:team' })<PricingTable for="organization" /><OrganizationProfile />references/b2b-patterns.mdhas({ plan })useSubscription()sessionClaimsimport { auth } from '@clerk/nextjs/server'
export default async function AccountPage() {
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>
)
}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>
)
}is for display only. For authorization checks (gating content or routes), always useuseSubscription()orhas({ plan }).has({ feature })
has({ plan })useSubscription()sessionClaimsimport { auth } from '@clerk/nextjs/server'
export default async function AccountPage() {
const { has } = await auth()
const currentPlan = has({ plan: 'pro' })
? 'pro'
: has({ plan: 'starter' })
? 'starter'
: 'free'
return (
<div>
<h2>当前套餐</h2>
<p>你正在使用{currentPlan}套餐</p>
{currentPlan === 'free' && <a href="/pricing">立即升级</a>}
</div>
)
}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">选择套餐</a>
return (
<div>
<p>状态: {subscription.status}</p>
{subscription.nextPayment && (
<p>下次支付日期: {subscription.nextPayment.date.toLocaleDateString()}</p>
)}
</div>
)
}仅用于展示。授权校验(管控内容或路由)时,请始终使用useSubscription()或has({ plan })。has({ feature })
auth()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' })
}auth()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: '需专业版套餐' }, { status: 403 })
}
return NextResponse.json({ data: '高级数据' })
}Clerk event names differ from Stripe event names. Clerk billing webhooks use dot-notation and camelCase, not Stripe's underscore format.There is noevent. Cancellation fires at the item level assubscription.canceled.subscriptionItem.canceled
Intent Stripe event name Clerk event name Subscription created customer.subscription.createdsubscription.createdSubscription updated customer.subscription.updatedsubscription.updatedSubscription active (none) subscription.activeSubscription past due (none) subscription.pastDueSubscription item canceled customer.subscription.deletedsubscriptionItem.canceledSubscription item past due invoice.payment_failedsubscriptionItem.pastDueSubscription item updated (none) subscriptionItem.updatedSubscription item active (none) subscriptionItem.activeSubscription item upcoming renewal (none) subscriptionItem.upcomingSubscription item ended (none) subscriptionItem.endedSubscription item abandoned (none) subscriptionItem.abandonedSubscription item expired (none) subscriptionItem.expiredSubscription item incomplete (none) subscriptionItem.incompleteFree trial ending soon (none) subscriptionItem.freeTrialEndingPayment attempt created (none) paymentAttempt.createdPayment attempt updated (none) paymentAttempt.updatedAlways use Clerk's event names, never Stripe's, inchecks.evt.type
Payload shape. Clerk billing webhook payloads are nested. The subscribing entity lives under(fields:evt.data.payer,user_id?). The plan info is on each item underorganization_id?. The subscription id is simplyevt.data.items[i].plan.slug. Subscription items do not carry aevt.data.idfield back-reference, so insubscription_idhandlers you identify the record by the item id (subscriptionItem.*) or look up by payer plus plan.evt.data.id
@clerk/nextjs/webhooksimport { 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 })
}@clerk/backendproxy.tsreferences/billing-webhooks.mdClerk事件名称与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请始终使用Clerk的事件名称,切勿在校验中使用Stripe的事件名称。evt.type
负载结构。Clerk账单Webhook负载为嵌套结构。订阅实体位于下(字段:evt.data.payer,user_id?)。套餐信息位于每个条目下的organization_id?。订阅ID为evt.data.items[i].plan.slug。订阅条目不包含evt.data.id反向引用字段,因此在subscription_id处理器中,需通过条目ID(subscriptionItem.*)或按支付方+套餐查询来识别记录。evt.data.id
@clerk/nextjs/webhooksimport { 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('验证失败', { 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 },
})
}
// 根据上述事件目录添加更多分支(如subscription.updated、
// subscriptionItem.canceled、subscriptionItem.pastDue等)
return new Response('成功', { status: 200 })
}@clerk/backendproxy.tsreferences/billing-webhooks.mdimport { PricingTable } from '@clerk/nextjs'
import { auth } from '@clerk/nextjs/server'
export default async function BillingPage() {
const { has } = await auth()
const isPro = has({ plan: 'pro' })
return (
<div>
<h1>Billing</h1>
{isPro ? (
<div>
<p>You are on the Pro plan</p>
<PricingTable />
</div>
) : (
<div>
<p>Upgrade to access premium features</p>
<PricingTable />
</div>
)}
</div>
)
}<PricingTable />import { PricingTable } from '@clerk/nextjs'
import { auth } from '@clerk/nextjs/server'
export default async function BillingPage() {
const { has } = await auth()
const isPro = has({ plan: 'pro' })
return (
<div>
<h1>账单管理</h1>
{isPro ? (
<div>
<p>你正在使用专业版套餐</p>
<PricingTable />
</div>
) : (
<div>
<p>升级以访问高级功能</p>
<PricingTable />
</div>
)}
</div>
)
}<PricingTable />| Tier | Plan Slug | Example Features |
|---|---|---|
| Free | (no plan check needed) | basic features |
| Starter | | |
| Pro | | |
| Enterprise | | all features + |
| 套餐层级 | 计划Slug | 示例功能 |
|---|---|---|
| 免费版 | (无需套餐校验) | 基础功能 |
| 入门版 | | |
| 专业版 | | |
| 企业版 | | 全部功能 + |
| Scenario | Who subscribes | Plan check |
|---|---|---|
| B2C SaaS | Individual user | |
| B2B SaaS | Organization | |
| Seat-limited B2B | Organization | Plan has a seat cap; pricing is per-plan, not per-member, tier your plans for bigger orgs |
has()| 场景 | 订阅主体 | 套餐校验方式 |
|---|---|---|
| B2C SaaS | 个人用户 | 在用户会话中使用 |
| B2B SaaS | 组织 | 在组织会话中使用 |
| 有限席位B2B | 组织 | 套餐设置席位上限;按套餐定价而非按成员,为大型组织设计分层套餐 |
has()<PricingTable /><CheckoutButton /><PricingTable />'use server'
import { redirect } from 'next/navigation'
export async function upgradeAction() {
redirect('/pricing')
}<PricingTable /><CheckoutButton /><PricingTable />'use server'
import { redirect } from 'next/navigation'
export async function upgradeAction() {
redirect('/pricing')
}| Error / symptom | Root cause | Fix |
|---|---|---|
| Billing is not enabled for this instance | Enable Billing at dashboard.clerk.com → Billing → Settings. No CLI path. |
| No plans, OR plan in the wrong tab (User vs Organization), OR Billing not enabled | Create plan in matching tab; pass |
| Users can't subscribe to a personal plan on a B2C + B2B app | Membership required mode (default since 2025-08-22) disables personal accounts, signed-in users are forced into | If you need personal + org subscriptions coexisting: Dashboard → Organizations settings → Membership optional |
| Can't find a Features page | Features are per-plan, not global | Dashboard → Billing → Plans → click plan → Features |
| Session token hasn't been refreshed to include the new plan | |
| Plan slug mismatch (case-sensitive), OR Billing not enabled, OR payment gateway not connected in production | Verify slug in Dashboard → Billing → Plans; confirm Billing → Settings shows enabled + connected gateway |
| The Feature tied to that permission is not included in the organization's active Plan | Add the Feature to the Plan in Dashboard → Billing → Plans → Features |
| Webhook 401 / signature verification failed | | Copy the Signing Secret from Dashboard → Webhooks; add the webhook route to |
| 错误/症状 | 根本原因 | 修复方案 |
|---|---|---|
| 当前实例未启用Billing功能 | 在dashboard.clerk.com → Billing → 设置启用Billing功能。无CLI路径。 |
| 无计划,或计划在错误标签页(用户/组织),或未启用Billing功能 | 在对应标签页创建计划;B2B场景传入 |
| 同时支持B2C+B2B的应用中,用户无法订阅个人套餐 | 强制会员模式(2025-08-22起默认启用)禁用个人账户,登录用户会被强制进入 | 如需同时支持个人+组织订阅:仪表盘 → 组织设置 → 可选会员 |
| 找不到功能页面 | 功能按计划划分,而非全局 | 仪表盘 → Billing → 计划 → 点击对应计划 → 功能模块 |
成功结账后 | 会话令牌未刷新以包含新套餐信息 | 调用 |
未订阅时 | 计划slug不匹配(区分大小写),或未启用Billing功能,或生产环境未连接支付网关 | 在仪表盘 → Billing → 计划中验证slug;确认Billing → 设置显示已启用且已连接网关 |
拥有对应角色的用户 | 该权限关联的功能未包含在组织的活跃套餐中 | 在仪表盘 → Billing → 计划 → 功能模块中,将功能添加到对应套餐 |
| Webhook返回401 / 签名验证失败 | | 从仪表盘 → Webhooks复制签名密钥;将Webhook路由添加到 |
has({ permission: 'org:posts:edit' })falsehas({ permission: 'org:posts:edit' })falseclerk-setupclerk-orgsclerk-webhooksclerk-setupclerk-orgsclerk-webhooks