clerk/skillsチェック済み
SKILL DETAIL
clerk-billing
clerk/skills/clerk-billing
Clerk Billing for subscription management - render Clerk's PricingTable
インストール · 119出典を見る
Installation
npx skills add https://github.com/clerk/skills --skill clerk-billing
スキルファイル
SKILL.md
最終同期 · 2026/09/06
evals/evals.json›
{
"skill_name": "clerk-billing",
"evals": [
{
"id": 1,
"prompt": "i have a next.js saas app with clerk auth. i need to add a pricing page that shows subscription plans and lets users subscribe. connect clerk billing and render the pricing table.",
"expected_output": "A /pricing route renders <PricingTable /> from @clerk/nextjs, with instructions to enable Billing in the Clerk Dashboard (dev gateway or Stripe for production) and a version-pinning note for the experimental APIs.",
"scaffold": "nextjs-basic-auth",
"files": [],
"assertions": [
"Creates app/pricing/page.tsx that imports and renders <PricingTable /> from '@clerk/nextjs'",
"Recommends pinning the Clerk SDK and clerk-js versions because billing APIs are experimental",
"States that Billing must be enabled in the Clerk Dashboard → Billing → Settings as a MANUAL step BEFORE rendering <PricingTable /> (surfaces the dashboard.clerk.com link or the error signature 'cannot_render_billing_disabled')",
"Does not claim that the Clerk CLI or Backend API can enable billing (the billing_settings toggle is Dashboard-only today)",
"States that plans must be created in the matching tab (User Plans for B2C / Organization Plans for B2B); slugs are scoped per tab",
"Does not claim that plans sync to Stripe Products (Clerk Billing is a separate product from Stripe Billing)",
"Does not import '@clerk/billing' as a separate package (billing APIs ship inside @clerk/nextjs)",
"Does not suggest manually calling stripe.checkout.sessions.create or any Stripe API method directly",
"Does not claim that PricingTable redirects to Stripe Checkout (it opens Clerk's in-app checkout drawer)"
]
},
{
"id": 2,
"prompt": "i need to protect my /dashboard/analytics route so only users on the 'pro' plan can access it. free users should be redirected to /pricing.",
"expected_output": "Server component at app/dashboard/analytics/page.tsx calls await auth(), uses has({ plan: 'pro' }) to check entitlement, and redirects free users to /pricing.",
"scaffold": "nextjs-basic-auth",
"files": [],
"assertions": [
"Imports auth from '@clerk/nextjs/server'",
"Calls await auth() (awaited, not sync)",
"Uses has({ plan: 'pro' }) to check the subscription",
"Calls redirect('/pricing') when the check fails",
"Does not read sessionClaims or JWT fields manually to check the plan"
]
},
{
"id": 3,
"prompt": "my b2b saas lets teams subscribe via their organization. i need per-seat billing where adding org members automatically increments the seat count. show me the org-level plan check to protect team-only routes.",
"expected_output": "Server component checks orgId is present then uses has({ plan: 'org:team' }) to gate org-scoped routes. Explanation CORRECTS the user's mental model: Clerk Billing uses seat-LIMIT plans (membership caps with fixed per-plan pricing), not Stripe-style per-seat metered billing where the charge scales per member. Recommends tiered plans for charging larger orgs more, and renders the B2B pricing page with <PricingTable for=\"organization\" />.",
"scaffold": "nextjs-basic-auth",
"files": [],
"assertions": [
"Destructures both orgId and has from await auth()",
"Checks that orgId exists before checking the plan",
"Uses has({ plan: 'org:team' }) for the org-level entitlement",
"Redirects to a billing or pricing page when the org lacks the plan",
"Corrects the user's assumption of auto-incrementing per-seat billing, states Clerk uses seat-LIMIT plans (fixed price per plan + membership cap), not metered pricing that scales per member",
"Suggests creating tiered plans (e.g. starter 5 seats, team 10, enterprise unlimited) as the way to charge bigger orgs more",
"Recommends <PricingTable for=\"organization\" /> for the B2B pricing page",
"States that the plan must be created under the 'Organization Plans' tab, not 'User Plans'",
"Does not claim Clerk creates one Stripe subscription item per member, or that seat count equals items.length",
"Does not suggest building a manual member counter or seat tracking table"
]
},
{
"id": 4,
"prompt": "i want to handle clerk billing webhook events so i can sync subscription status to my database. i need to handle when a subscription is created, when it gets canceled, and when a payment fails or goes past due.",
"expected_output": "POST handler calls verifyWebhook(req) from @clerk/nextjs/webhooks, branches on Clerk's dot-camelCase event names (subscription.created, subscriptionItem.canceled, subscriptionItem.pastDue), and proxy/middleware marks /api/webhooks as public.",
"scaffold": "nextjs-basic-auth",
"files": [],
"assertions": [
"Imports verifyWebhook from '@clerk/nextjs/webhooks' (the Clerk-provided helper)",
"Does NOT import Webhook from 'svix' directly, uses Clerk's verifyWebhook wrapper instead",
"Branches on the exact Clerk event name 'subscription.created'",
"Branches on the exact Clerk event name 'subscriptionItem.canceled' (camelCase, not 'subscription.canceled' or 'subscriptionItem.cancelled')",
"Branches on the exact Clerk event name 'subscriptionItem.pastDue' (camelCase 'pastDue', NOT snake_case 'past_due')",
"Does not use Stripe event names like 'customer.subscription.created', 'invoice.payment_failed', or 'customer.subscription.deleted'",
"If it inspects evt.data.object, matches against 'commerce_subscription' / 'commerce_subscription_item' / 'commerce_payer' / 'commerce_payment_attempt', not 'billing_*' variants (the wire format uses commerce_, not billing_)",
"Updates proxy.ts or middleware.ts to add /api/webhooks(.*) to createRouteMatcher so the webhook route is public",
"The clerkMiddleware callback is marked async to support await auth.protect()"
]
},
{
"id": 5,
"prompt": "i need a billing settings page where users can see their current plan and upgrade or downgrade their subscription. show current status and let them change plans.",
"expected_output": "A protected billing page that renders <PricingTable /> for plan changes and displays the current plan using has({ plan }) or useSubscription().",
"scaffold": "nextjs-basic-auth",
"files": [],
"assertions": [
"Page is guarded by await auth() and requires authentication",
"Determines the current plan using has({ plan }) or the useSubscription() hook",
"Does not read sessionClaims.metadata.plan to display the current plan",
"Renders <PricingTable /> on the same page for plan changes",
"Does not implement a custom Stripe Customer Portal integration from scratch",
"Handles the case where the user has no active subscription"
]
},
{
"id": 6,
"prompt": "i have three plans: free, starter, and pro. i want to gate individual features: 'export' is pro-only, 'analytics' is starter and pro. show how to conditionally render ui elements based on feature entitlements, not plan tiers.",
"expected_output": "Uses has({ feature: 'export' }) and has({ feature: 'analytics' }) (not has({ plan })), explains the decision rule for feature vs plan gating, warns about the billing-gates-permissions behavior, and notes that features are created per-plan (inside each plan's edit page in Dashboard → Billing → Plans), not in a global Features page.",
"scaffold": "nextjs-basic-auth",
"files": [],
"assertions": [
"Uses has({ feature: 'export' }) for the export gate (feature, not plan)",
"Uses has({ feature: 'analytics' }) for the analytics gate (feature, not plan)",
"Never uses has({ plan: ... }) to gate these specific capabilities",
"Explains the decision rule: use has({ feature }) for individual capabilities, use has({ plan }) only for tier-level gates like a Pro dashboard",
"States that features live inside each plan's edit page, not a global Features page (/billing/features does not exist)",
"States that the same feature slug can be attached to multiple plans, has({ feature }) matches if the active plan contains that slug",
"Warns that when Billing is enabled, has({ permission: ... }) returns false if the required Feature is not attached to the active Plan (billing gates permissions)",
"Does not suggest reading sessionClaims.metadata.plan or parsing plan tier strings directly"
]
},
{
"id": 7,
"prompt": "users are subscribing but i'm not seeing their plan in the has() check. how do i debug why has({ plan: 'pro' }) returns false after a successful stripe checkout?",
"expected_output": "A debugging checklist: verify exact plan slug, confirm Billing is enabled (and Stripe connected for production), refresh the session after checkout, inspect Billing → Subscriptions in Dashboard.",
"scaffold": "nextjs-basic-auth",
"files": [],
"assertions": [
"Mentions that the plan slug in code must match exactly what is defined in Dashboard → Billing → Plans",
"Mentions that Billing must be enabled in Dashboard → Billing → Settings (dev gateway for development, Stripe for production)",
"Mentions that the session needs to refresh after checkout to include the new plan",
"Suggests inspecting Clerk Dashboard → Billing → Subscriptions to verify the subscription exists",
"Does not suggest building a custom subscription tracking system as a workaround"
]
},
{
"id": 8,
"prompt": "i need a server action that creates a checkout session for upgrading a user from free to pro. the action should redirect to stripe checkout.",
"expected_output": "Explanation that PricingTable handles checkout; if a server action is shown, it redirects to a page that renders PricingTable rather than calling Stripe's checkout API.",
"scaffold": "nextjs-basic-auth",
"files": [],
"assertions": [
"Primary recommendation uses <PricingTable /> (or <CheckoutButton /> from @clerk/nextjs/experimental for a targeted single-plan flow)",
"Does not call stripe.checkout.sessions.create or equivalent Stripe API methods directly",
"If a server action is shown, it redirects to a /pricing route that renders PricingTable",
"States that PricingTable renders plan selection and opens Clerk's in-app checkout drawer (no redirect to Stripe Checkout)",
"Does not replace Clerk's billing integration with a raw Stripe implementation"
]
},
{
"id": 9,
"prompt": "i'm trying to use clerk billing in my production app but i can't find the billing section in the dashboard and has({ plan }) always returns false. is billing broken?",
"expected_output": "Explanation that Clerk Billing must be explicitly enabled in Dashboard → Billing → Settings (this is Dashboard-only, the CLI and Backend API cannot toggle it). Mention that <PricingTable /> will throw 'cannot_render_billing_disabled' in development when this step is skipped.",
"scaffold": "nextjs-basic-auth",
"files": [],
"assertions": [
"States that Billing is not enabled by default and must be explicitly enabled in Dashboard → Billing → Settings",
"Surfaces the Dashboard link (dashboard.clerk.com) as the only way to toggle billing today",
"Mentions the 'cannot_render_billing_disabled' error code or the '<PricingTable /> cannot be rendered when billing is disabled' message as the developer-facing signal",
"Explains that has({ plan }) returns false when Billing is not enabled regardless of any Stripe configuration",
"Does not claim the Clerk CLI, clerk config patch, Backend API, or Platform API can enable billing",
"Does not diagnose this as a code bug before confirming Billing is enabled in Dashboard"
]
},
{
"id": 10,
"prompt": "i want to offer a 14-day free trial on my pro plan that automatically converts to a paid subscription. how do i set this up with clerk billing?",
"expected_output": "Configure the trial period in Clerk Dashboard → Billing → Plans → Pro. Clerk handles the trial-to-paid conversion automatically (Stripe only processes the payment). Listen for subscriptionItem.freeTrialEnding to notify users.",
"scaffold": "nextjs-basic-auth",
"files": [],
"assertions": [
"Directs user to configure the trial period in Clerk Dashboard → Billing → Plans",
"States that Clerk handles the trial-to-paid conversion automatically (Stripe's role is payment processing only)",
"Mentions the subscriptionItem.freeTrialEnding webhook for notifying users before conversion",
"Recommends <PricingTable /> as the entry point for users to start the trial",
"Does not suggest building a custom trial tracking table or cron job"
]
},
{
"id": 11,
"prompt": "i need to charge customers based on the number of api calls they make each month. how do i implement metered/usage-based billing with clerk?",
"expected_output": "Explanation that Clerk Billing currently supports fixed subscription plans (no native metered pricing). For usage-gated SaaS, use has({ feature }) to gate allotments and track consumption in your own datastore; do not bypass Clerk by configuring billing directly in Stripe, because Clerk Plans and Subscriptions do not sync with Stripe Billing.",
"scaffold": "nextjs-basic-auth",
"files": [],
"assertions": [
"Acknowledges that Clerk Billing primarily handles fixed subscription plans (no native metered primitive)",
"Does not claim Clerk Billing has a native metered pricing primitive",
"Does not instruct the user to configure metered pricing directly in Stripe and report usage via Stripe's usage records API as a replacement for Clerk Billing",
"Notes that Clerk Plans and Subscriptions do not sync to Stripe Billing, so bypassing Clerk to use Stripe metered billing would split the source of truth",
"Uses has({ feature }) to gate features based on plan allotments, with usage tracked in the developer's own datastore"
]
},
{
"id": 12,
"prompt": "my enterprise customers need downloadable invoices and a billing portal where they can update payment methods. how do i build this with clerk?",
"expected_output": "Render <UserProfile /> (B2C) or <OrganizationProfile /> (B2B), both include built-in billing management with invoice history and payment methods.",
"scaffold": "nextjs-basic-auth",
"files": [],
"assertions": [
"Uses <UserProfile /> (B2C) or <OrganizationProfile /> (B2B) for billing management UI",
"States that Clerk provides built-in invoice history accessible through the profile component",
"States that payment method management is handled by the profile component",
"Does not build a custom Stripe Customer Portal integration from scratch",
"For enterprise B2B scenarios, recommends OrganizationProfile over UserProfile"
]
}
]
}
references/b2b-patterns.md›
# B2B Billing Patterns
## Overview
B2B billing in Clerk attaches subscriptions to **organizations**, not individual users. Each org gets its own subscription. Plans can carry a **seat limit** (membership cap) which Clerk enforces on member invites.
> **Create the plan as an Organization Plan, not a User Plan.** Use [Dashboard → Billing → Plans](https://dashboard.clerk.com/last-active?path=billing/plans) (Organization Plans tab) or `clerk config patch` with `billing.plans`. Slugs are scoped per type. A `team` plan registered under User Plans will not appear in `<PricingTable for="organization" />`, and vice versa. Plan type cannot be changed after creation, recreate if misplaced.
## Core Pattern: Org-Level Plan Check
```typescript
import { auth } from '@clerk/nextjs/server'
import { redirect } from 'next/navigation'
export default async function TeamDashboard() {
const { orgId, has } = await auth()
if (!orgId) {
redirect('/sign-in')
}
if (!has({ plan: 'org:team' })) {
redirect('/billing')
}
return <TeamFeatures />
}
```
**Always check `orgId` first.** If the user has no active org, `has({ plan })` evaluates against the user's personal subscription (which may not exist).
## Seat-Limit Plans
Clerk Billing's B2B model is **seat-limit plans**: each organization plan has a fixed price and an optional membership cap; Clerk enforces the cap at invite/join time. To charge larger orgs more, create tiered plans (e.g. `starter` capped at 5, `team` at 10, `enterprise` unlimited) with increasing fixed prices.
Key invariants:
- **Fixed price per plan**, not auto-scaling per member. Adding members does not increment the org's billing amount on the active plan.
- **One `active` SubscriptionItem per payer per Plan.** Do not derive seat count from `items.length`.
- **Seat limit is a Plan property.** Set it when creating the plan (Dashboard → Billing → Plans → Organization Plans tab, or `clerk config patch`); it cannot be changed later.
- When an org exceeds or changes to a plan with a lower limit, existing members stay but new invites are blocked until the org is under cap. See [Plans with seat limits](https://clerk.com/docs/guides/billing/seat-limit-plans) for the exact admin behavior.
No custom seat-counting code is needed. Read the active plan with `has({ plan: 'org:team' })` and let Clerk enforce membership limits.
## Org Billing Page
Use `<OrganizationProfile />` for the org account billing UI. It renders the active org plan, members, invitations, and the upgrade / cancellation flow scoped to the active organization, with admin-only access to billing actions enforced by Clerk:
```tsx
import { OrganizationProfile } from '@clerk/nextjs'
export default function OrgAccountPage() {
return <OrganizationProfile />
}
```
Organization Plans configured in Dashboard → Billing → Plans automatically appear inside `<OrganizationProfile />` (in the **Plans** section). Only org admins see the billing controls. Build a custom page only when you need branded layouts or to embed `<PricingTable for="organization" />` outside the OrganizationProfile shell.
## Webhook: Org Subscription Events
```typescript
if (evt.type === 'subscription.created') {
const { id, payer, items, status } = evt.data
if (payer.organization_id) {
const plan = items[0]?.plan?.slug
await db.orgSubscriptions.upsert({
where: { orgId: payer.organization_id },
create: {
orgId: payer.organization_id,
plan,
subscriptionId: id,
status,
},
update: { plan, subscriptionId: id, status },
})
}
}
if (evt.type === 'subscription.updated') {
const { id, payer, items, status } = evt.data
if (payer.organization_id) {
const plan = items[0]?.plan?.slug
await db.orgSubscriptions.update({
where: { orgId: payer.organization_id },
data: { plan, status },
})
}
}
```
Use `payer.organization_id` (nested under `payer`, not a top-level `org_id`) when the subscription belongs to an organization. Do NOT use `items.length` as a seat count, seat limits are set at the plan level and there is only one active SubscriptionItem per payer per Plan.
## Plan Naming for B2B
Tier plans by seat cap so bigger orgs pay more:
| Plan | Slug | Seat cap |
|------|------|-------|
| Startup | `org:starter` | 5 |
| Team | `org:team` | 10 |
| Business | `org:business` | 25 |
| Enterprise | `org:enterprise` | unlimited (requires B2B Authentication add-on) |
Define these via Dashboard → Billing → Plans → **Organization Plans** tab with **Seat-based** toggled on, or via `clerk config patch` with `billing.plans`. Use the `org:` prefix in slugs to disambiguate org plans from user plans in code (`has({ plan: 'org:team' })` vs `has({ plan: 'team' })`). Seat caps above 20 and "unlimited" require the B2B Authentication add-on.
## Common Mistake: Checking Plan Without Active Org
```typescript
// WRONG, user has no active org, has() checks user subscription
const { has } = await auth()
if (!has({ plan: 'org:team' })) redirect('/billing')
// CORRECT, check orgId first
const { orgId, has } = await auth()
if (!orgId) redirect('/sign-in')
if (!has({ plan: 'org:team' })) redirect('/billing')
```
references/b2c-patterns.md›
# B2C Billing Patterns
## Overview
B2C billing in Clerk attaches subscriptions to **individual users**. Each user gets their own Clerk subscription. Use `has({ plan })` on the user session.
> **Prerequisite: personal accounts must be allowed.** If Organizations are enabled, open [Dashboard → Organizations settings](https://dashboard.clerk.com/last-active?path=organizations-settings) and set **Membership options → "Membership optional"**. In "Membership required" mode personal accounts are disabled, `<PricingTable />` silently excludes any user without an active org (no error, no console warning). Check this first when a user reports "subscribe does nothing."
Plans for B2C must be created as **User Plans** (Dashboard → Billing → Plans → User Plans tab, or `clerk config patch` with `billing.plans`). A `pro` plan registered as an Organization Plan is a separate entity and won't appear in `<PricingTable />`. Plan type isn't changeable, recreate if misplaced.
## Core Pattern: User Plan Check
```typescript
import { auth } from '@clerk/nextjs/server'
import { redirect } from 'next/navigation'
export default async function ProDashboard() {
const { userId, has } = await auth()
if (!userId) {
redirect('/sign-in')
}
if (!has({ plan: 'pro' })) {
redirect('/pricing')
}
return <ProContent />
}
```
## Tiered Feature Gating
Prefer `has({ feature })` over `has({ plan })` for capability gating: features can be reattached between plans without a code deploy.
```typescript
import { auth } from '@clerk/nextjs/server'
export default async function AppPage() {
const { has } = await auth()
const canAnalytics = has({ feature: 'analytics' })
const canExport = has({ feature: 'export' })
const canApi = has({ feature: 'api_access' })
return (
<div>
<BasicFeature />
{canAnalytics && <AnalyticsDashboard />}
{canExport && <ExportButton />}
{canApi && <APIAccess />}
</div>
)
}
```
## Redirect Pattern After Checkout
After checkout completes in Clerk's drawer, Clerk refreshes the session with updated plan data. Pass `newSubscriptionRedirectUrl` to `<PricingTable />` to navigate after the user confirms. For custom post-checkout logic:
```typescript
// app/billing/success/page.tsx
import { auth } from '@clerk/nextjs/server'
import { redirect } from 'next/navigation'
export default async function BillingSuccessPage() {
const { has } = await auth()
if (!has({ plan: 'pro' })) {
return <p>Waiting for subscription to activate...</p>
}
redirect('/dashboard')
}
```
## Account Billing Page
Use `<UserProfile />` for the user account billing UI. It renders the current plan, subscription status, payment methods, invoices, and the upgrade / cancellation flow with no custom code:
```tsx
import { UserProfile } from '@clerk/nextjs'
export default function AccountPage() {
return <UserProfile />
}
```
User Plans configured in Dashboard → Billing → Plans automatically appear inside `<UserProfile />` (in the **Plans** section). Cancellation and plan switching are handled in the same drawer. Only build a custom billing page when you need branded layouts or to embed `<PricingTable />` outside the UserProfile shell.
For richer subscription details in client components (status, renewal date, trial end), use the `useSubscription()` hook instead of reading JWT claims:
```tsx
'use client'
import { useSubscription } from '@clerk/nextjs/experimental'
export function BillingSummary() {
const { data, isLoading } = useSubscription()
if (isLoading || !data) return null
return (
<p>
Status: {data.status}
{data.nextPayment && ` (renews ${data.nextPayment.date.toLocaleDateString()})`}
</p>
)
}
```
## Client-Side Feature Gating
For interactive components that need plan checks:
```tsx
'use client'
import { useAuth } from '@clerk/nextjs'
export function ExportButton() {
const { has } = useAuth()
if (!has?.({ plan: 'pro' })) {
return (
<button disabled title="Pro plan required">
Export (Pro)
</button>
)
}
return <button onClick={handleExport}>Export</button>
}
```
Note: `has` may be `undefined` on initial render. Use optional chaining `has?.()`.
references/billing-components.md›
# Billing Components
## PricingTable
The primary billing UI component. Renders subscription plans and opens Clerk's in-app checkout drawer when a user selects a plan.
```tsx
import { PricingTable } from '@clerk/nextjs'
export default function PricingPage() {
return <PricingTable />
}
```
**What it does:**
- Fetches plan data from Clerk Dashboard configuration
- Renders plan cards with features, pricing, and CTA buttons
- Opens Clerk's in-app **checkout drawer** on plan selection
- Shows current plan for subscribed users with upgrade/downgrade options
**Key behaviors:**
- Respects the active entity by default: user subscription for personal accounts, org subscription in org context. Override with `for`.
- For an account-management UI that includes plan switching, cancellations, payment methods, and invoices, render `<UserProfile />` (B2C) or `<OrganizationProfile />` (B2B) instead. Use `<PricingTable />` standalone only when you need a dedicated pricing page.
**Props (all optional):**
| Prop | Type | Description |
|------|------|-------------|
| `appearance` | `Appearance` | Style overrides for the component. |
| `checkoutProps` | `{ appearance: Appearance }` | Style overrides for the checkout drawer. |
| `collapseFeatures` | `boolean` | Start with features collapsed. Requires `layout: 'default'`. Defaults to `false`. |
| `ctaPosition` | `'top' \| 'bottom'` | Placement of the CTA button. Requires `layout: 'default'`. Defaults to `'bottom'`. |
| `fallback` | `JSX` | UI to show while the pricing table is loading. |
| `for` | `'user' \| 'organization'` | Which subscriber the table targets. Defaults to `'user'`. **Use `'organization'` for B2B.** |
| `newSubscriptionRedirectUrl` | `string` | URL to navigate to after checkout completes. |
For most cases, use `<PricingTable />` with no props (or just `for="organization"` for B2B).
## CheckoutButton
For custom checkout UX outside `<PricingTable />`, use `<CheckoutButton />` from `@clerk/nextjs/experimental`. It opens the same checkout drawer for a specific `planId`. Must be wrapped in `<Show when="signed-in">`.
```tsx
'use client'
import { Show } from '@clerk/nextjs'
import { CheckoutButton } from '@clerk/nextjs/experimental'
export function UpgradeCTA() {
return (
<Show when="signed-in">
<CheckoutButton
planId="cplan_xxx"
planPeriod="annual"
for="user"
newSubscriptionRedirectUrl="/dashboard"
onSubscriptionComplete={() => console.log('subscribed')}
/>
</Show>
)
}
```
Key props: `planId` (required), `planPeriod: 'month' | 'annual'`, `for: 'user' | 'organization'`, `onSubscriptionComplete`, `newSubscriptionRedirectUrl`, `checkoutProps: { appearance }`. Throws if `for="organization"` is set with no active Organization.
## has(), Plan and Feature Checks
`has()` is returned by `auth()` (server) and `useAuth()` (client). It checks subscription plans and feature entitlements.
### Server usage
```typescript
import { auth } from '@clerk/nextjs/server'
const { has } = await auth()
has({ plan: 'pro' }) // true if user/org is on 'pro' plan
has({ feature: 'export' }) // true if user/org has 'export' feature entitlement
```
### Client usage
```tsx
'use client'
import { useAuth } from '@clerk/nextjs'
const { has } = useAuth()
has?.({ plan: 'pro' }) // optional chaining, may be undefined before hydration
has?.({ feature: 'export' })
```
### Combining with role checks
```typescript
const { has, orgRole } = await auth()
const canManageBilling = has({ plan: 'pro' }) && orgRole === 'org:admin'
```
### What `has()` checks
`has()` checks the **active session claims**. Plans and features are embedded in the Clerk session token after a subscription is active. The check does NOT make a network request.
If a user subscribes and then `has()` returns false, the session token needs to refresh. This happens automatically on the next page load or after calling `clerk.session.reload()` on the client.
## Show Component (Billing-aware)
```tsx
import { Show } from '@clerk/nextjs'
<Show when={{ plan: 'pro' }}>
<ProFeatures />
</Show>
<Show when={{ feature: 'analytics' }}>
<AnalyticsDashboard />
</Show>
```
`<Show>` accepts `plan` and `feature` props in addition to `role` and `permission`. Renders children only if the condition is met.
## Import Reference
```typescript
// Stable
import { PricingTable, Show, useAuth } from '@clerk/nextjs'
import { auth } from '@clerk/nextjs/server'
// Experimental (Billing)
import { CheckoutButton, useSubscription } from '@clerk/nextjs/experimental'
```
Stable billing primitives (`PricingTable`, `has()` via `auth()` / `useAuth()`, `<Show>`) live in `@clerk/nextjs`. Experimental hooks and components (`useSubscription`, `CheckoutButton`, `SubscriptionDetailsButton`, `PlanDetailsButton`) live in `@clerk/nextjs/experimental`. No separate billing package needed.
references/billing-webhooks.md›
# Billing Webhooks
## Setup
Billing webhooks use the same `verifyWebhook(req)` pattern as all Clerk webhooks. Register the endpoint in Clerk Dashboard → Webhooks.
Subscribe to the billing events you care about. Full catalog:
Subscription events (4):
- `subscription.created`
- `subscription.updated`
- `subscription.active`
- `subscription.pastDue`
SubscriptionItem events (10):
- `subscriptionItem.updated`
- `subscriptionItem.active`
- `subscriptionItem.canceled`
- `subscriptionItem.upcoming`
- `subscriptionItem.ended`
- `subscriptionItem.expired`
- `subscriptionItem.abandoned`
- `subscriptionItem.incomplete`
- `subscriptionItem.pastDue`
- `subscriptionItem.freeTrialEnding`
Payment attempt events (2):
- `paymentAttempt.created`
- `paymentAttempt.updated`
> There is no `subscription.canceled` event. Cancellation fires at the item level as `subscriptionItem.canceled`.
## Payload Shape (important)
Clerk billing webhook payloads are nested. Common mistakes come from destructuring fields that live deeper than the top level. The canonical shape:
- `evt.data.id`: the subscription or subscription item id (use this as the subscription id reference)
- `evt.data.payer`: the subscribing entity, with `user_id?` and `organization_id?` (not `org_id`)
- `evt.data.status`: server-side status string (snake_case, e.g. `past_due`, even though event names are camelCase)
- `evt.data.items[]`: on subscription events only, the array of subscription items
- `evt.data.items[i].plan.slug`: the plan slug for a given item (plan is a nested object)
- On subscriptionItem events, the event data IS the item. There is no `subscription_id` back-reference, so match by `payer` + `plan.slug` or persist item ids independently.
## Complete Billing Webhook Handler
```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' || evt.type === 'subscription.active') {
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 },
})
}
if (evt.type === 'subscription.updated') {
const { id, payer, items, status } = evt.data
const entityId = payer.organization_id ?? payer.user_id
const plan = items[0]?.plan?.slug
await db.subscriptions.update({
where: { subscriptionId: id },
data: { entityId, plan, status },
})
}
if (evt.type === 'subscription.pastDue') {
const { id, status } = evt.data
await db.subscriptions.update({
where: { subscriptionId: id },
data: { status },
})
}
if (evt.type === 'subscriptionItem.canceled') {
// Subscription item events carry only the item, not its parent subscription id.
// Identify the record by payer + plan slug.
const { payer, plan, canceled_at } = evt.data
const entityId = payer?.organization_id ?? payer?.user_id
await db.subscriptionItems.update({
where: { entityId, plan: plan?.slug },
data: { status: 'canceled', canceledAt: canceled_at },
})
// Notify user/org admin of cancellation
}
if (evt.type === 'subscriptionItem.pastDue') {
const { payer, plan, past_due_at } = evt.data
const entityId = payer?.organization_id ?? payer?.user_id
await db.subscriptionItems.update({
where: { entityId, plan: plan?.slug },
data: { status: 'past_due', pastDueAt: past_due_at },
})
// Notify user/org admin of payment failure
}
return new Response('OK', { status: 200 })
}
```
## Make Route Public
In `proxy.ts` (Next.js <=15: `middleware.ts`):
```typescript
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'
const isPublicRoute = createRouteMatcher(['/api/webhooks(.*)'])
export default clerkMiddleware(async (auth, req) => {
if (!isPublicRoute(req)) await auth.protect()
})
```
## Event Payload Reference
Types come from `BillingSubscriptionWebhookEventJSON` and `BillingSubscriptionItemWebhookEventJSON` in `@clerk/backend`. Key fields:
### subscription.created / subscription.updated / subscription.active / subscription.pastDue
```typescript
{
type: 'subscription.created',
data: {
object: 'commerce_subscription',
id: string, // subscription id
status: 'active' | 'past_due' | 'canceled' | 'ended' | 'abandoned' | 'incomplete' | 'expired' | 'upcoming',
active_at?: number,
canceled_at?: number,
ended_at?: number,
past_due_at?: number,
created_at: number,
updated_at: number,
latest_payment_id: string,
payer_id: string,
payer: {
object: 'commerce_payer',
id: string,
user_id?: string, // set for B2C subscriptions
organization_id?: string, // set for B2B subscriptions
email: string,
first_name?: string,
last_name?: string,
organization_name?: string,
// ...
},
payment_source_id: string,
items: Array<{ // subscription items, one active item per payer per Plan
id: string,
status: string,
plan?: { id, name, slug, amount, period, ... },
period_start: number,
period_end: number | null,
canceled_at?: number,
past_due_at?: number,
// ...
}>,
}
}
```
### subscriptionItem.canceled / subscriptionItem.pastDue / subscriptionItem.*
The event data IS the item itself, not the parent subscription:
```typescript
{
type: 'subscriptionItem.canceled',
data: {
object: 'commerce_subscription_item',
id: string, // subscription item id
status: string,
period_start: number,
period_end: number | null,
canceled_at?: number,
past_due_at?: number,
plan?: { id, slug, name, amount, period, ... },
plan_id?: string | null,
payer?: { user_id?, organization_id?, email, ... },
amount: { amount, amount_formatted, currency, currency_symbol },
// ...
}
}
```
### paymentAttempt.created / paymentAttempt.updated
```typescript
{
type: 'paymentAttempt.created',
data: {
object: 'commerce_payment_attempt',
id: string,
status: 'pending' | 'paid' | 'failed',
charge_type: 'checkout' | 'recurring',
failed_reason?: { code: string, decline_code: string },
paid_at?: number,
failed_at?: number,
payer: { user_id?, organization_id?, ... },
subscription_items: Array<{ /* same shape as subscriptionItem events */ }>,
// ...
}
}
```
## Key Rules
- The subscribing entity lives at `evt.data.payer`, with `user_id?` (B2C) or `organization_id?` (B2B)
- The subscription id is `evt.data.id` on subscription events, not a separate `subscription_id` field
- Plan slug is nested: `evt.data.items[i].plan?.slug` on subscription events, `evt.data.plan?.slug` on item events
- Status values use snake_case (`past_due`, `active`, `canceled`), even though event names use camelCase (`subscription.pastDue`)
- Always return `200` quickly. Handle async work in a queue or background job.
- Use `upsert` in `subscription.created` handlers to tolerate webhook replays
- `CLERK_WEBHOOK_SIGNING_SECRET` must match the Signing Secret from the Clerk Dashboard endpoint
## Subscription Status Values
| Status | Meaning |
|--------|---------|
| `active` | Subscription is active and paid |
| `past_due` | Payment failed, grace period |
| `canceled` | Subscription ended |
| `ended` | Subscription reached the end of its term |
| `abandoned` | Checkout started but user never completed payment |
| `incomplete` | Checkout in progress |
| `expired` | Subscription expired without renewal |
| `upcoming` | Scheduled subscription not yet active |
SKILL.md›
---
name: clerk-billing
description: Clerk Billing for subscription management - render Clerk's PricingTable
and in-app checkout drawer, configure subscription plans, seat-limit plans for
B2B, feature entitlements with has(), and billing webhooks. Use for SaaS
monetization, plan gating, checkout flows, trials, invoicing, and subscription
lifecycle management.
allowed-tools: WebFetch
license: MIT
compatibility: Requires NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY, CLERK_SECRET_KEY, and CLERK_WEBHOOK_SIGNING_SECRET. Billing must be enabled in Clerk Dashboard → Billing. Development instances can use the shared Clerk development gateway; production instances require a Stripe account for payment processing.
metadata:
author: clerk
version: 1.1.0
---
# Billing
> **STOP, prerequisite.** Billing must be enabled before any `<PricingTable />`, `<CheckoutButton />`, `has({ plan })`, or `has({ feature })` usage works. Two paths: (1) [Dashboard → Billing → Settings](https://dashboard.clerk.com/last-active?path=billing/settings), or (2) `clerk enable billing` (see "Agent-first: Programmatic billing config" below). Enabling auto-creates default `free_user` / `free_org` plans. 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.
## Quick Start
1. **Enable Billing**, via [Dashboard → Billing → Settings](https://dashboard.clerk.com/last-active?path=billing/settings) or `clerk enable billing` (see Agent-first section). Skipping this throws `cannot_render_billing_disabled` in dev and renders empty in prod.
2. **Create plans in the matching tab**, [Dashboard → Billing → Plans](https://dashboard.clerk.com/last-active?path=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.
## Dashboard shortcuts
| Action | URL |
|---|---|
| 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 features | Plans → click a plan → Features section (no direct URL) |
## Agent-first: Programmatic billing config
The full billing config (enable toggles, plans, features, plan-feature attachments) is editable via PLAPI without touching the Dashboard. Useful for agents seeding plans, replicating config across instances, or version-controlling billing structure.
Pre-req: project linked to the Clerk app (`clerk auth login` + `clerk link`, see `clerk-setup`). Billing is account-only — unlike orgs, it cannot be enabled on an unclaimed app; claim the app first.
### Enable Billing via CLI
```bash
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
```
### Pull current billing config
```bash
clerk config pull --keys billing > billing.json
```
This writes the current billing config (toggles + plans + features) for the linked instance to `billing.json`.
### Edit and apply
Edit `billing.json` to add/remove plans or features, then preview the diff and apply:
```bash
clerk config patch --file billing.json --dry-run
clerk config patch --file billing.json
```
Pass `--instance prod` to target the production instance instead of dev.
### Raw PATCH (full control)
For one-shot plan/feature updates without a config file:
```bash
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"}]}}'
```
### Notes
- This handles **billing config** (toggles + plans + features catalog). **Subscription lifecycle** (users picking a plan, checkout, renewal, cancellation) still flows through `<PricingTable />` + billing webhooks, see `clerk-webhooks` skill for the lifecycle events.
- Top-level `features` map manipulation and plan-feature attachments (sync) are fully supported via the PLAPI billing config handler.
## What Do You Need?
| Task | Reference |
|------|-----------|
| `<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 templates | references/billing-webhooks.md |
## References
| Reference | Description |
|-----------|-------------|
| `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 |
## Documentation
- [Billing overview](https://clerk.com/docs/guides/billing/overview)
- [B2B SaaS billing](https://clerk.com/docs/guides/billing/for-b2b)
- [B2C SaaS billing](https://clerk.com/docs/guides/billing/for-b2c)
- [Billing webhooks](https://clerk.com/docs/guides/development/webhooks/billing)
## 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.
| Scenario | Correct 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.
## Key Patterns
### 1. Render the Pricing Table
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.
### 2. Check Feature Entitlements (Server-Side)
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.
### 3. Check Feature Entitlements (Client-Side)
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 })`.
### 4. Check Subscription Plan Server-Side
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 />
}
```
### 5. Client-Side Plan Checks
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>
)
}
```
### 6. B2B Seat-Based Billing with Organizations
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.
### 7. Display Subscription Status
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 })`.
### 8. Protect API Routes by Plan
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' })
}
```
### 9. Handle Billing Webhooks
> **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`.
>
> | Intent | Stripe event name | Clerk 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`.
### 10. Upgrade / Downgrade Flow
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.
## Plan and Feature Naming
Plan slugs and feature slugs are defined in Clerk Dashboard → Billing. Common conventions:
| Tier | Plan Slug | Example 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.
## B2B vs B2C Billing
| Scenario | Who subscribes | Plan check |
|----------|---------------|------------|
| B2C SaaS | Individual user | `has({ plan: 'pro' })` on user session |
| B2B SaaS | Organization | `has({ plan: 'org:team' })` on org session |
| Seat-limited B2B | Organization | Plan 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).
## 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')
}
```
## 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 / symptom | Root cause | Fix |
|---|---|---|
| `Clerk: 🔒 The <PricingTable/> component cannot be rendered when billing is disabled.` (code: `cannot_render_billing_disabled`, dev only) | Billing is not enabled for this instance | Enable Billing at [dashboard.clerk.com → Billing → Settings](https://dashboard.clerk.com/last-active?path=billing/settings), or run `clerk enable billing`. |
| `<PricingTable />` renders empty | No plans, OR plan in the wrong tab (User vs Organization), OR Billing not enabled | Create plan in matching tab; pass `for="organization"` for B2B; check Billing Settings |
| 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 `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 page | Features are per-plan, not global | Dashboard → 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 production | Verify 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 Plan | Add 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(.*)'])` |
## 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.
## 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