clerk-billing

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Billing

Billing

STOP, Dashboard-only prerequisite. Billing must be enabled from the Clerk Dashboard before any
<PricingTable />
,
<CheckoutButton />
,
has({ plan })
, or
has({ feature })
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.
Note: Billing APIs are still experimental. Pin your
@clerk/nextjs
and
clerk-js
package versions. See
clerk
skill for the supported version table.
⚠️ 注意:仅支持通过仪表盘配置。在使用任何
<PricingTable />
<CheckoutButton />
has({ plan })
has({ feature })
之前,必须先在Clerk仪表盘中启用Billing功能。目前Clerk CLI和后端API不提供启用开关,唯一路径是dashboard.clerk.com → 你的应用 → Billing → Settings。开发环境可使用共享的Clerk开发网关(无需Stripe账户);生产环境仅需绑定Stripe账户用于支付处理。
提示:Billing API仍处于实验阶段。请固定
@clerk/nextjs
clerk-js
的包版本。查看
clerk
技能文档获取支持版本列表。

Quick Start

快速开始

  1. Enable Billing, Dashboard → Billing → Settings. Dashboard-only; no CLI or API path. Skipping this throws
    cannot_render_billing_disabled
    in dev and renders empty in prod.
  2. Create plans in the matching tab, Dashboard → Billing → Plans. Two tabs, slugs scoped per tab, not movable after creation:
    • User Plans
      <PricingTable />
      (default
      for="user"
      )
    • Organization Plans
      <PricingTable for="organization" />
    Wrong-tab is the #1 cause of an empty
    <PricingTable />
    . Plans live in Clerk; not synced to Stripe.
  3. Add features inside a plan, open the plan in Dashboard → Billing → Plans, use its Features section. Features are scoped per plan, not global. The same slug can attach to multiple plans;
    has({ feature: 'export' })
    matches if the active plan contains that slug.
  4. Render
    <PricingTable />
    (pass
    for="organization"
    for B2B).
  5. Gate access with
    has({ plan })
    or
    has({ feature })
    from
    auth()
    .
  6. Handle billing webhooks for subscription lifecycle.
  1. 启用Billing功能,访问仪表盘 → Billing → 设置。仅支持仪表盘配置;无CLI或API路径。跳过此步骤会在开发环境抛出
    cannot_render_billing_disabled
    错误,生产环境则渲染空内容。
  2. 在对应标签页创建计划,访问仪表盘 → Billing → 计划。分为两个标签页,计划slug按标签页划分,创建后不可移动:
    • 用户计划
      <PricingTable />
      (默认
      for="user"
    • 组织计划
      <PricingTable for="organization" />
    选错标签页是
    <PricingTable />
    渲染为空的最常见原因。计划存储在Clerk中;不会同步到Stripe。
  3. 在计划内添加功能,在仪表盘 → Billing → 计划中打开对应计划,使用其功能模块。功能按计划划分,而非全局。同一slug可绑定多个计划;
    has({ feature: 'export' })
    会匹配包含该slug的任何活跃计划。
  4. 渲染
    <PricingTable />
    (B2B场景需传入
    for="organization"
    )。
  5. 使用
    auth()
    中的
    has({ plan })
    has({ feature })
    管控访问权限
  6. 处理账单Webhook以管理订阅生命周期

Dashboard shortcuts

仪表盘快捷入口

ActionURL
Enable Billing
https://dashboard.clerk.com/last-active?path=billing/settings
Create / edit plans
https://dashboard.clerk.com/last-active?path=billing/plans
Membership mode (B2C + B2B coexistence)
https://dashboard.clerk.com/last-active?path=organizations-settings
Edit featuresPlans → click a plan → Features section (no direct URL)
操作链接
启用Billing功能
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
编辑功能计划 → 点击对应计划 → 功能模块(无直接链接)

What Do You Need?

所需参考文档

TaskReference
<PricingTable />
props,
<CheckoutButton />
,
<Show>
billing patterns
references/billing-components.md
B2C patterns (individual user subscriptions,
Membership optional
prerequisite)
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 templatesreferences/billing-webhooks.md
任务参考链接
<PricingTable />
属性、
<CheckoutButton />
<Show>
账单模式
references/billing-components.md
B2C模式(个人用户订阅,需先设置“可选会员”)references/b2c-patterns.md
B2B模式(组织订阅、席位限制计划、管理员专属账单UI)references/b2b-patterns.md
Webhook事件目录、负载结构、处理器模板references/billing-webhooks.md

References

参考文档说明

ReferenceDescription
references/billing-components.md
<PricingTable />
and subscription UI
references/b2c-patterns.md
B2C subscription billing patterns
references/b2b-patterns.md
B2B billing with organization subscriptions and seat-limit plans
references/billing-webhooks.md
Subscription lifecycle event handling
参考文档描述
references/billing-components.md
<PricingTable />
与订阅UI组件说明
references/b2c-patterns.md
B2C订阅账单实现模式
references/b2b-patterns.md
基于组织订阅和席位限制计划的B2B账单方案
references/billing-webhooks.md
订阅生命周期事件处理指南

Documentation

官方文档

Features vs Plans: When to Use Which

功能与计划:何时使用哪种方式

Use
has({ feature: 'slug' })
when gating a specific capability
, export, analytics, API access, audit logs.
Use
has({ plan: 'slug' })
when gating a tier
, showing the pro dashboard, checking org subscription level, redirecting free users.
ScenarioCorrect check
Gate the "Export CSV" button
has({ feature: 'export' })
Gate the "Analytics" section
has({ feature: 'analytics' })
Gate all of /dashboard/pro
has({ plan: 'pro' })
Check if org has team subscription
has({ plan: 'org:team' })
Gate SSO configuration
has({ feature: 'sso' })
When a user says "gate the export feature" or "gate analytics", always use
has({ feature })
. Only use
has({ plan })
when the gate is the plan tier itself, not a specific capability within it.
当管控特定功能(如导出、分析、API访问、审计日志)时,使用
has({ feature: 'slug' })
当管控整个套餐层级(如专业版仪表盘、检查组织订阅等级、引导免费用户升级)时,使用
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 })

Key Patterns

核心实现模式

1. Render the Pricing Table

1. 渲染价格表

Show available plans to users with a single component:
tsx
import { PricingTable } from '@clerk/nextjs'

export default function PricingPage() {
	return (
		<main>
			<h1>Choose a plan</h1>
			<PricingTable />
		</main>
	)
}
<PricingTable />
automatically renders all plans configured in the Clerk Dashboard. Selecting a plan opens Clerk's in-app checkout drawer. No props needed for basic usage. For B2B, pass
for="organization"
to render org-level plans instead of user plans.
使用单个组件向用户展示可用计划:
tsx
import { PricingTable } from '@clerk/nextjs'

export default function PricingPage() {
	return (
		<main>
			<h1>选择你的套餐</h1>
			<PricingTable />
		</main>
	)
}
<PricingTable />
会自动渲染Clerk仪表盘中配置的所有计划。选择计划后会打开Clerk的应用内结账抽屉。基础使用无需传入属性。B2B场景需传入
for="organization"
以渲染组织级计划,而非用户计划。

2. Check Feature Entitlements (Server-Side)

2. 服务端校验功能权限

Gate by individual features, this is the preferred approach for specific capabilities:
typescript
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>
	)
}
Features are configured in Clerk Dashboard → Billing → Features and assigned to plans. Use
has({ feature })
instead of
has({ plan })
when gating granular capabilities, check the feature, not the plan.
按单个功能管控访问权限,这是特定功能管控的推荐方式:
typescript
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>
	)
}
功能需在Clerk仪表盘 → Billing → 功能中配置并分配给对应计划。管控细粒度功能时,使用
has({ feature })
而非
has({ plan })
,直接校验功能而非套餐。

3. Check Feature Entitlements (Client-Side)

3. 客户端校验功能权限

Use
useAuth()
for client-side feature gating. Combine with server-side checks for full coverage:
tsx
'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>
	)
}
Server Components use
auth()
, Client Components use
useAuth()
. Both support
has({ feature })
and
has({ plan })
.
使用
useAuth()
进行客户端功能管控。结合服务端校验实现全面覆盖:
tsx
'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. Check Subscription Plan Server-Side

4. 服务端校验订阅计划

Gate access by subscription plan (use this for tier-level gates, not individual features):
typescript
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 />
}
按订阅套餐管控访问权限(仅用于套餐层级管控,而非单个功能):
typescript
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 />
}

5. Client-Side Plan Checks

5. 客户端校验订阅计划

Use
useAuth()
hook for client components:
tsx
'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()
钩子实现客户端组件的套餐校验:
tsx
'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>
	)
}

6. B2B Seat-Based Billing with Organizations

6. B2B基于席位的组织账单

Org plans can carry a seat limit (membership cap) that Clerk enforces at invite time. Use the
org:
slug prefix on org-side plan checks (e.g.
has({ plan: 'org:team' })
) to keep gating unambiguous. Render the B2B pricing page with
<PricingTable for="organization" />
, and use
<OrganizationProfile />
for the org account billing UI.
See
references/b2b-patterns.md
for tiered plan naming, seat-limit invariants, admin-only billing, and webhook handlers.
组织计划可设置席位限制(成员上限),Clerk会在邀请成员时自动强制执行。在组织侧套餐校验中使用
org:
前缀的slug(如
has({ plan: 'org:team' })
),确保权限管控清晰。使用
<PricingTable for="organization" />
渲染B2B价格页,使用
<OrganizationProfile />
作为组织账户的账单UI。
查看
references/b2b-patterns.md
获取分层套餐命名、席位限制规则、管理员专属账单及Webhook处理器的详细说明。

7. Display Subscription Status

7. 展示订阅状态

Check specific plans with
has({ plan })
, or use
useSubscription()
for full subscription details in client components. Do not read plan information from
sessionClaims
directly, that is not the supported path.
Server component, check for specific plans:
typescript
import { 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>
	)
}
Client component, full subscription details via
useSubscription()
:
tsx
'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()
is for display only. For authorization checks (gating content or routes), always use
has({ plan })
or
has({ feature })
.
使用
has({ plan })
校验特定套餐,或在客户端组件中使用
useSubscription()
获取完整订阅详情。请勿直接从
sessionClaims
读取套餐信息,这并非官方支持的方式。
服务端组件,校验特定套餐:
typescript
import { 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()
获取完整订阅详情:
tsx
'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 })

8. Protect API Routes by Plan

8. 按套餐保护API路由

Gate API routes using
auth()
:
typescript
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()
管控API路由:
typescript
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: '高级数据' })
}

9. Handle Billing Webhooks

9. 处理账单Webhook

Clerk event names differ from Stripe event names. Clerk billing webhooks use dot-notation and camelCase, not Stripe's underscore format.
There is no
subscription.canceled
event. Cancellation fires at the item level as
subscriptionItem.canceled
.
IntentStripe event nameClerk event name
Subscription created
customer.subscription.created
subscription.created
Subscription updated
customer.subscription.updated
subscription.updated
Subscription active(none)
subscription.active
Subscription past due(none)
subscription.pastDue
Subscription item canceled
customer.subscription.deleted
subscriptionItem.canceled
Subscription item past due
invoice.payment_failed
subscriptionItem.pastDue
Subscription item updated(none)
subscriptionItem.updated
Subscription item active(none)
subscriptionItem.active
Subscription item upcoming renewal(none)
subscriptionItem.upcoming
Subscription item ended(none)
subscriptionItem.ended
Subscription item abandoned(none)
subscriptionItem.abandoned
Subscription item expired(none)
subscriptionItem.expired
Subscription item incomplete(none)
subscriptionItem.incomplete
Free trial ending soon(none)
subscriptionItem.freeTrialEnding
Payment attempt created(none)
paymentAttempt.created
Payment attempt updated(none)
paymentAttempt.updated
Always use Clerk's event names, never Stripe's, in
evt.type
checks.
Payload shape. Clerk billing webhook payloads are nested. The subscribing entity lives under
evt.data.payer
(fields:
user_id?
,
organization_id?
). The plan info is on each item under
evt.data.items[i].plan.slug
. The subscription id is simply
evt.data.id
. Subscription items do not carry a
subscription_id
field back-reference, so in
subscriptionItem.*
handlers you identify the record by the item id (
evt.data.id
) or look up by payer plus plan.
Minimal handler to anchor the pattern (import from
@clerk/nextjs/webhooks
, verify, branch on Clerk event name):
typescript
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 })
}
For the full template covering all 15 events, the TS type declarations from
@clerk/backend
, the
proxy.ts
public-route setup, and the subscription status value table, see
references/billing-webhooks.md
.
Clerk事件名称与Stripe不同。Clerk账单Webhook使用点标记和驼峰命名,而非Stripe的下划线格式。
不存在
subscription.canceled
事件。取消订阅会在条目级别触发
subscriptionItem.canceled
事件。
操作意图Stripe事件名称Clerk事件名称
订阅创建
customer.subscription.created
subscription.created
订阅更新
customer.subscription.updated
subscription.updated
订阅激活(无)
subscription.active
订阅逾期(无)
subscription.pastDue
订阅条目取消
customer.subscription.deleted
subscriptionItem.canceled
订阅条目逾期
invoice.payment_failed
subscriptionItem.pastDue
订阅条目更新(无)
subscriptionItem.updated
订阅条目激活(无)
subscriptionItem.active
订阅条目即将续订(无)
subscriptionItem.upcoming
订阅条目结束(无)
subscriptionItem.ended
订阅条目放弃(无)
subscriptionItem.abandoned
订阅条目过期(无)
subscriptionItem.expired
订阅条目未完成(无)
subscriptionItem.incomplete
免费试用即将结束(无)
subscriptionItem.freeTrialEnding
支付尝试创建(无)
paymentAttempt.created
支付尝试更新(无)
paymentAttempt.updated
请始终使用Clerk的事件名称,切勿在
evt.type
校验中使用Stripe的事件名称。
负载结构。Clerk账单Webhook负载为嵌套结构。订阅实体位于
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事件名称分支处理):
typescript
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('验证失败', { 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 })
}
如需覆盖全部15个事件的完整模板、
@clerk/backend
提供的TS类型声明、
proxy.ts
公共路由配置以及订阅状态值表,请查看
references/billing-webhooks.md

10. Upgrade / Downgrade Flow

10. 升级/降级流程

Let users manage their subscription from inside the app:
tsx
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>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 />
renders differently for subscribed users, it shows the current plan and allows upgrades or cancellations, all through Clerk's in-app checkout drawer.
允许用户在应用内管理订阅:
tsx
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 />
针对已订阅用户会显示不同内容:展示当前套餐并允许升级或取消,所有操作均通过Clerk的应用内结账抽屉完成。

Plan and Feature Naming

计划与功能命名规范

Plan slugs and feature slugs are defined in Clerk Dashboard → Billing. Common conventions:
TierPlan SlugExample Features
Free(no plan check needed)basic features
Starter
starter
analytics
,
api_access
Pro
pro
analytics
,
export
,
team
Enterprise
enterprise
all features +
sso
,
audit_logs
Use lowercase slugs matching what you define in the dashboard.
计划slug和功能slug在Clerk仪表盘 → Billing中定义。常见命名规范:
套餐层级计划Slug示例功能
免费版(无需套餐校验)基础功能
入门版
starter
analytics
,
api_access
专业版
pro
analytics
,
export
,
team
企业版
enterprise
全部功能 +
sso
,
audit_logs
使用小写slug,与仪表盘中定义的内容一致。

B2B vs B2C Billing

B2B与B2C账单对比

ScenarioWho subscribesPlan check
B2C SaaSIndividual user
has({ plan: 'pro' })
on user session
B2B SaaSOrganization
has({ plan: 'org:team' })
on org session
Seat-limited B2BOrganizationPlan has a seat cap; pricing is per-plan, not per-member, tier your plans for bigger orgs
For B2B, ensure the user has an active org session. The
has()
check evaluates the active entity (user or org).
场景订阅主体套餐校验方式
B2C SaaS个人用户在用户会话中使用
has({ plan: 'pro' })
B2B SaaS组织在组织会话中使用
has({ plan: 'org:team' })
有限席位B2B组织套餐设置席位上限;按套餐定价而非按成员,为大型组织设计分层套餐
B2B场景需确保用户拥有活跃的组织会话。
has()
校验会基于当前活跃实体(用户或组织)进行判断。

Checkout Flows

结账流程

Clerk renders its own checkout drawer automatically through
<PricingTable />
and
<CheckoutButton />
. Plans and pricing live in Clerk. To trigger checkout from a server action, redirect to a page that renders
<PricingTable />
:
typescript
'use server'
import { redirect } from 'next/navigation'

export async function upgradeAction() {
	redirect('/pricing')
}
Clerk会通过
<PricingTable />
<CheckoutButton />
自动渲染内置的结账抽屉。计划和价格存储在Clerk中。如需从服务端操作触发结账,请重定向到渲染
<PricingTable />
的页面:
typescript
'use server'
import { redirect } from 'next/navigation'

export async function upgradeAction() {
	redirect('/pricing')
}

Error Signatures (diagnose fast)

错误排查速查

When you see any of these errors or symptoms, the fix is almost always a Dashboard toggle, not a code change. Do not start editing components.
Error / symptomRoot causeFix
Clerk: 🔒 The <PricingTable/> component cannot be rendered when billing is disabled.
(code:
cannot_render_billing_disabled
, dev only)
Billing is not enabled for this instanceEnable Billing at dashboard.clerk.com → Billing → Settings. No CLI path.
<PricingTable />
renders empty
No plans, OR plan in the wrong tab (User vs Organization), OR Billing not enabledCreate plan in matching tab; pass
for="organization"
for B2B; check Billing Settings
Users can't subscribe to a personal plan on a B2C + B2B appMembership required mode (default since 2025-08-22) disables personal accounts, signed-in users are forced into
choose-organization
and never land on a personal-subscription state
If you need personal + org subscriptions coexisting: Dashboard → Organizations settings → Membership optional
Can't find a Features pageFeatures are per-plan, not globalDashboard → Billing → Plans → click plan → Features
has({ plan: 'pro' })
always returns
false
after a successful checkout
Session token hasn't been refreshed to include the new plan
await clerk.session?.reload()
or navigate to force a new session
has({ plan: 'pro' })
returns
false
before any subscribe attempt
Plan slug mismatch (case-sensitive), OR Billing not enabled, OR payment gateway not connected in productionVerify slug in Dashboard → Billing → Plans; confirm Billing → Settings shows enabled + connected gateway
has({ permission: 'org:x:y' })
returns
false
for a user who has the role
The Feature tied to that permission is not included in the organization's active PlanAdd the Feature to the Plan in Dashboard → Billing → Plans → Features
Webhook 401 / signature verification failed
CLERK_WEBHOOK_SIGNING_SECRET
mismatch or route protected by middleware
Copy the Signing Secret from Dashboard → Webhooks; add the webhook route to
createRouteMatcher(['/api/webhooks(.*)'])
遇到以下错误或症状时,解决方案几乎都是通过仪表盘设置,而非修改代码。请勿直接编辑组件。
错误/症状根本原因修复方案
Clerk: 🔒 The <PricingTable/> component cannot be rendered when billing is disabled.
(错误码:
cannot_render_billing_disabled
,仅开发环境)
当前实例未启用Billing功能dashboard.clerk.com → Billing → 设置启用Billing功能。无CLI路径。
<PricingTable />
渲染为空
无计划,或计划在错误标签页(用户/组织),或未启用Billing功能在对应标签页创建计划;B2B场景传入
for="organization"
;检查Billing设置
同时支持B2C+B2B的应用中,用户无法订阅个人套餐强制会员模式(2025-08-22起默认启用)禁用个人账户,登录用户会被强制进入
choose-organization
流程,无法进入个人订阅状态
如需同时支持个人+组织订阅:仪表盘 → 组织设置 → 可选会员
找不到功能页面功能按计划划分,而非全局仪表盘 → Billing → 计划 → 点击对应计划 → 功能模块
成功结账后
has({ plan: 'pro' })
始终返回
false
会话令牌未刷新以包含新套餐信息调用
await clerk.session?.reload()
或导航到其他页面以获取新会话
未订阅时
has({ plan: 'pro' })
返回
false
计划slug不匹配(区分大小写),或未启用Billing功能,或生产环境未连接支付网关在仪表盘 → Billing → 计划中验证slug;确认Billing → 设置显示已启用且已连接网关
拥有对应角色的用户
has({ permission: 'org:x:y' })
返回
false
该权限关联的功能未包含在组织的活跃套餐中在仪表盘 → Billing → 计划 → 功能模块中,将功能添加到对应套餐
Webhook返回401 / 签名验证失败
CLERK_WEBHOOK_SIGNING_SECRET
不匹配,或路由被中间件保护
从仪表盘 → Webhooks复制签名密钥;将Webhook路由添加到
createRouteMatcher(['/api/webhooks(.*)'])

Billing Gates Permissions

账单管控权限

When Billing is enabled,
has({ permission: 'org:posts:edit' })
returns
false
if the Feature associated with that permission is not included in the organization's active Plan, even if the user has the permission assigned via their role. This is by design: billing gates permissions at the feature level. Always ensure the required Feature is attached to the Plan in Dashboard → Billing → Plans → Features.
启用Billing后,若组织的活跃套餐未包含权限关联的功能,即使用户通过角色获得了该权限,
has({ permission: 'org:posts:edit' })
仍会返回
false
。这是设计预期:账单会在功能层面管控权限。请确保所需功能已添加到仪表盘 → Billing → 计划 → 功能模块中的对应套餐。

See Also

相关技能

  • clerk-setup
    - Initial Clerk install
  • clerk-orgs
    - B2B organizations (required for B2B billing and seat-limit plans)
  • clerk-webhooks
    - Webhook signature verification and routing
  • clerk-setup
    - Clerk初始安装配置
  • clerk-orgs
    - B2B组织管理(B2B账单和席位限制计划的前置依赖)
  • clerk-webhooks
    - Webhook签名验证与路由配置