← ClerkCONTENT HISTORYWHAT CHANGED · RULE-BASED ANALYSIS
Update to Clerk
Snapshot Sep 30, 2026 · 23:09 UTC · version 0.1.0
Collection source: not recorded for this historical snapshot.
First saved snapshot
No earlier snapshot is available to establish a change.
Compare saved observations
Download comparison JSONFull technical diff · 0 changed fields
Full snapshot data
{
"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.",
"included_files": [
{
"relative_path": "evals/evals.json",
"size_in_bytes": 15403
},
{
"relative_path": "references/b2b-patterns.md",
"size_in_bytes": 5173
},
{
"relative_path": "references/b2c-patterns.md",
"size_in_bytes": 4169
},
{
"relative_path": "references/billing-components.md",
"size_in_bytes": 4871
},
{
"relative_path": "references/billing-webhooks.md",
"size_in_bytes": 7799
}
],
"skill_md_contents": "---\nname: clerk-billing\ndescription: Clerk Billing for subscription management - render Clerk's PricingTable\n and in-app checkout drawer, configure subscription plans, seat-limit plans for\n B2B, feature entitlements with has(), and billing webhooks. Use for SaaS\n monetization, plan gating, checkout flows, trials, invoicing, and subscription\n lifecycle management.\nallowed-tools: WebFetch\nlicense: MIT\ncompatibility: 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.\nmetadata:\n author: clerk\n version: 1.1.0\n---\n\n# Billing\n\n> **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.\n>\n> **Note**: Billing APIs are still experimental. Pin your `@clerk/nextjs` and `clerk-js` package versions. See `clerk` skill for the supported version table.\n\n## Quick Start\n\n1. **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.\n2. **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:\n - **User Plans** → `<PricingTable />` (default `for=\"user\"`)\n - **Organization Plans** → `<PricingTable for=\"organization\" />`\n\n Wrong-tab is the #1 cause of an empty `<PricingTable />`. Plans live in Clerk; not synced to Stripe.\n3. **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.\n4. **Render `<PricingTable />`** (pass `for=\"organization\"` for B2B).\n5. **Gate access** with `has({ plan })` or `has({ feature })` from `auth()`.\n6. **Handle billing webhooks** for subscription lifecycle.\n\n## Dashboard shortcuts\n\n| Action | URL |\n|---|---|\n| Enable Billing | `https://dashboard.clerk.com/last-active?path=billing/settings` |\n| Create / edit plans | `https://dashboard.clerk.com/last-active?path=billing/plans` |\n| Membership mode (B2C + B2B coexistence) | `https://dashboard.clerk.com/last-active?path=organizations-settings` |\n| Edit features | Plans → click a plan → Features section (no direct URL) |\n\n## Agent-first: Programmatic billing config\n\nThe 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.\n\nPre-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.\n\n### Enable Billing via CLI\n\n```bash\nclerk enable billing # both targets (default, auto-creates free_user + free_org plans)\nclerk enable billing --for org # org only\nclerk enable billing --for user # user only\n```\n\n### Pull current billing config\n\n```bash\nclerk config pull --keys billing > billing.json\n```\n\nThis writes the current billing config (toggles + plans + features) for the linked instance to `billing.json`.\n\n### Edit and apply\n\nEdit `billing.json` to add/remove plans or features, then preview the diff and apply:\n\n```bash\nclerk config patch --file billing.json --dry-run\nclerk config patch --file billing.json\n```\n\nPass `--instance prod` to target the production instance instead of dev.\n\n### Raw PATCH (full control)\n\nFor one-shot plan/feature updates without a config file:\n\n```bash\nclerk api --platform PATCH /v1/platform/applications/<app_id>/instances/<ins_id>/config \\\n -d '{\"billing\":{\"plans\":[{\"slug\":\"pro\",\"name\":\"Pro\",\"amount\":2000,\"currency\":\"usd\",\"payer_type\":\"user\",\"is_recurring\":true}],\"features\":[{\"slug\":\"export\",\"name\":\"Export\"}]}}'\n```\n\n### Notes\n\n- 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.\n- Top-level `features` map manipulation and plan-feature attachments (sync) are fully supported via the PLAPI billing config handler.\n\n## What Do You Need?\n\n| Task | Reference |\n|------|-----------|\n| `<PricingTable />` props, `<CheckoutButton />`, `<Show>` billing patterns | references/billing-components.md |\n| B2C patterns (individual user subscriptions, `Membership optional` prerequisite) | references/b2c-patterns.md |\n| B2B patterns (org subscriptions, seat-limit plans, admin-gated billing UI) | references/b2b-patterns.md |\n| Webhook event catalog, payload shapes, handler templates | references/billing-webhooks.md |\n\n## References\n\n| Reference | Description |\n|-----------|-------------|\n| `references/billing-components.md` | `<PricingTable />` and subscription UI |\n| `references/b2c-patterns.md` | B2C subscription billing patterns |\n| `references/b2b-patterns.md` | B2B billing with organization subscriptions and seat-limit plans |\n| `references/billing-webhooks.md` | Subscription lifecycle event handling |\n\n## Documentation\n\n- [Billing overview](https://clerk.com/docs/guides/billing/overview)\n- [B2B SaaS billing](https://clerk.com/docs/guides/billing/for-b2b)\n- [B2C SaaS billing](https://clerk.com/docs/guides/billing/for-b2c)\n- [Billing webhooks](https://clerk.com/docs/guides/development/webhooks/billing)\n\n## Features vs Plans: When to Use Which\n\n**Use `has({ feature: 'slug' })` when gating a specific capability**, export, analytics, API access, audit logs.\n\n**Use `has({ plan: 'slug' })` when gating a tier**, showing the pro dashboard, checking org subscription level, redirecting free users.\n\n| Scenario | Correct check |\n|----------|---------------|\n| Gate the \"Export CSV\" button | `has({ feature: 'export' })` |\n| Gate the \"Analytics\" section | `has({ feature: 'analytics' })` |\n| Gate all of /dashboard/pro | `has({ plan: 'pro' })` |\n| Check if org has team subscription | `has({ plan: 'org:team' })` |\n| Gate SSO configuration | `has({ feature: 'sso' })` |\n\nWhen 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.\n\n## Key Patterns\n\n### 1. Render the Pricing Table\n\nShow available plans to users with a single component:\n\n```tsx\nimport { PricingTable } from '@clerk/nextjs'\n\nexport default function PricingPage() {\n\treturn (\n\t\t<main>\n\t\t\t<h1>Choose a plan</h1>\n\t\t\t<PricingTable />\n\t\t</main>\n\t)\n}\n```\n\n`<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.\n\n### 2. Check Feature Entitlements (Server-Side)\n\nGate by individual features, this is the preferred approach for specific capabilities:\n\n```typescript\nimport { auth } from '@clerk/nextjs/server'\n\nexport default async function AnalyticsPage() {\n\tconst { has } = await auth()\n\n\tconst canViewAnalytics = has({ feature: 'analytics' })\n\tconst canExport = has({ feature: 'export' })\n\n\treturn (\n\t\t<div>\n\t\t\t{canViewAnalytics && <AnalyticsChart />}\n\t\t\t{canExport && <ExportButton />}\n\t\t</div>\n\t)\n}\n```\n\nFeatures 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.\n\n### 3. Check Feature Entitlements (Client-Side)\n\nUse `useAuth()` for client-side feature gating. Combine with server-side checks for full coverage:\n\n```tsx\n'use client'\nimport { useAuth } from '@clerk/nextjs'\n\nexport function FeatureGatedUI() {\n\tconst { has, isLoaded } = useAuth()\n\tif (!isLoaded) return null\n\n\tconst canExport = has?.({ feature: 'export' })\n\tconst canAnalytics = has?.({ feature: 'analytics' })\n\n\treturn (\n\t\t<div>\n\t\t\t{canAnalytics && <AnalyticsSection />}\n\t\t\t{canExport ? <ExportButton /> : <UpgradeToExport />}\n\t\t</div>\n\t)\n}\n```\n\nServer Components use `auth()`, Client Components use `useAuth()`. Both support `has({ feature })` and `has({ plan })`.\n\n### 4. Check Subscription Plan Server-Side\n\nGate access by subscription plan (use this for tier-level gates, not individual features):\n\n```typescript\nimport { auth } from '@clerk/nextjs/server'\nimport { redirect } from 'next/navigation'\n\nexport default async function ProDashboard() {\n\tconst { has } = await auth()\n\n\tif (!has({ plan: 'pro' })) {\n\t\tredirect('/pricing')\n\t}\n\n\treturn <ProFeatures />\n}\n```\n\n### 5. Client-Side Plan Checks\n\nUse `useAuth()` hook for client components:\n\n```tsx\n'use client'\nimport { useAuth } from '@clerk/nextjs'\n\nexport function UpgradePrompt() {\n\tconst { has } = useAuth()\n\n\tif (has?.({ plan: 'pro' })) {\n\t\treturn null\n\t}\n\n\treturn (\n\t\t<div>\n\t\t\t<p>Upgrade to Pro to access this feature</p>\n\t\t\t<a href=\"/pricing\">View plans</a>\n\t\t</div>\n\t)\n}\n```\n\n### 6. B2B Seat-Based Billing with Organizations\n\nOrg 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.\n\nSee `references/b2b-patterns.md` for tiered plan naming, seat-limit invariants, admin-only billing, and webhook handlers.\n\n### 7. Display Subscription Status\n\nCheck 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.\n\nServer component, check for specific plans:\n\n```typescript\nimport { auth } from '@clerk/nextjs/server'\n\nexport default async function AccountPage() {\n\tconst { has } = await auth()\n\n\tconst currentPlan = has({ plan: 'pro' })\n\t\t? 'pro'\n\t\t: has({ plan: 'starter' })\n\t\t\t? 'starter'\n\t\t\t: 'free'\n\n\treturn (\n\t\t<div>\n\t\t\t<h2>Current Plan</h2>\n\t\t\t<p>You are on the {currentPlan} plan</p>\n\t\t\t{currentPlan === 'free' && <a href=\"/pricing\">Upgrade</a>}\n\t\t</div>\n\t)\n}\n```\n\nClient component, full subscription details via `useSubscription()`:\n\n```tsx\n'use client'\nimport { useSubscription } from '@clerk/nextjs/experimental'\n\nexport function SubscriptionDetails() {\n\tconst { data: subscription, isLoading } = useSubscription()\n\tif (isLoading) return null\n\tif (!subscription) return <a href=\"/pricing\">Choose a plan</a>\n\n\treturn (\n\t\t<div>\n\t\t\t<p>Status: {subscription.status}</p>\n\t\t\t{subscription.nextPayment && (\n\t\t\t\t<p>Next payment: {subscription.nextPayment.date.toLocaleDateString()}</p>\n\t\t\t)}\n\t\t</div>\n\t)\n}\n```\n\n> `useSubscription()` is for display only. For authorization checks (gating content or routes), always use `has({ plan })` or `has({ feature })`.\n\n### 8. Protect API Routes by Plan\n\nGate API routes using `auth()`:\n\n```typescript\nimport { auth } from '@clerk/nextjs/server'\nimport { NextResponse } from 'next/server'\n\nexport async function GET() {\n\tconst { has } = await auth()\n\n\tif (!has({ plan: 'pro' })) {\n\t\treturn NextResponse.json({ error: 'Pro plan required' }, { status: 403 })\n\t}\n\n\treturn NextResponse.json({ data: 'premium data' })\n}\n```\n\n### 9. Handle Billing Webhooks\n\n> **Clerk event names differ from Stripe event names.** Clerk billing webhooks use dot-notation and camelCase, not Stripe's underscore format.\n>\n> There is no `subscription.canceled` event. Cancellation fires at the item level as `subscriptionItem.canceled`.\n>\n> | Intent | Stripe event name | Clerk event name |\n> |--------|------------------|-----------------|\n> | Subscription created | `customer.subscription.created` | `subscription.created` |\n> | Subscription updated | `customer.subscription.updated` | `subscription.updated` |\n> | Subscription active | (none) | `subscription.active` |\n> | Subscription past due | (none) | `subscription.pastDue` |\n> | Subscription item canceled | `customer.subscription.deleted` | `subscriptionItem.canceled` |\n> | Subscription item past due | `invoice.payment_failed` | `subscriptionItem.pastDue` |\n> | Subscription item updated | (none) | `subscriptionItem.updated` |\n> | Subscription item active | (none) | `subscriptionItem.active` |\n> | Subscription item upcoming renewal | (none) | `subscriptionItem.upcoming` |\n> | Subscription item ended | (none) | `subscriptionItem.ended` |\n> | Subscription item abandoned | (none) | `subscriptionItem.abandoned` |\n> | Subscription item expired | (none) | `subscriptionItem.expired` |\n> | Subscription item incomplete | (none) | `subscriptionItem.incomplete` |\n> | Free trial ending soon | (none) | `subscriptionItem.freeTrialEnding` |\n> | Payment attempt created | (none) | `paymentAttempt.created` |\n> | Payment attempt updated | (none) | `paymentAttempt.updated` |\n>\n> Always use Clerk's event names, never Stripe's, in `evt.type` checks.\n\n> **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.\n\nMinimal handler to anchor the pattern (import from `@clerk/nextjs/webhooks`, verify, branch on Clerk event name):\n\n```typescript\nimport { verifyWebhook } from '@clerk/nextjs/webhooks'\nimport { NextRequest } from 'next/server'\nimport { db } from '@/lib/db'\n\nexport async function POST(req: NextRequest) {\n\tlet evt\n\ttry {\n\t\tevt = await verifyWebhook(req)\n\t} catch {\n\t\treturn new Response('Verification failed', { status: 400 })\n\t}\n\n\tif (evt.type === 'subscription.created') {\n\t\tconst { id, payer, items, status } = evt.data\n\t\tconst entityId = payer.organization_id ?? payer.user_id\n\t\tconst plan = items[0]?.plan?.slug\n\t\tawait db.subscriptions.upsert({\n\t\t\twhere: { subscriptionId: id },\n\t\t\tcreate: { subscriptionId: id, entityId, plan, status },\n\t\t\tupdate: { entityId, plan, status },\n\t\t})\n\t}\n\n\t// Add more branches per the event catalog above (subscription.updated,\n\t// subscriptionItem.canceled, subscriptionItem.pastDue, etc.)\n\n\treturn new Response('OK', { status: 200 })\n}\n```\n\nFor 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`.\n\n### 10. Upgrade / Downgrade Flow\n\nLet users manage their subscription from inside the app:\n\n```tsx\nimport { PricingTable } from '@clerk/nextjs'\nimport { auth } from '@clerk/nextjs/server'\n\nexport default async function BillingPage() {\n\tconst { has } = await auth()\n\tconst isPro = has({ plan: 'pro' })\n\n\treturn (\n\t\t<div>\n\t\t\t<h1>Billing</h1>\n\t\t\t{isPro ? (\n\t\t\t\t<div>\n\t\t\t\t\t<p>You are on the Pro plan</p>\n\t\t\t\t\t<PricingTable />\n\t\t\t\t</div>\n\t\t\t) : (\n\t\t\t\t<div>\n\t\t\t\t\t<p>Upgrade to access premium features</p>\n\t\t\t\t\t<PricingTable />\n\t\t\t\t</div>\n\t\t\t)}\n\t\t</div>\n\t)\n}\n```\n\n`<PricingTable />` renders differently for subscribed users, it shows the current plan and allows upgrades or cancellations, all through Clerk's in-app checkout drawer.\n\n## Plan and Feature Naming\n\nPlan slugs and feature slugs are defined in Clerk Dashboard → Billing. Common conventions:\n\n| Tier | Plan Slug | Example Features |\n|------|-----------|-----------------|\n| Free | (no plan check needed) | basic features |\n| Starter | `starter` | `analytics`, `api_access` |\n| Pro | `pro` | `analytics`, `export`, `team` |\n| Enterprise | `enterprise` | all features + `sso`, `audit_logs` |\n\nUse lowercase slugs matching what you define in the dashboard.\n\n## B2B vs B2C Billing\n\n| Scenario | Who subscribes | Plan check |\n|----------|---------------|------------|\n| B2C SaaS | Individual user | `has({ plan: 'pro' })` on user session |\n| B2B SaaS | Organization | `has({ plan: 'org:team' })` on org session |\n| Seat-limited B2B | Organization | Plan has a seat cap; pricing is per-plan, not per-member, tier your plans for bigger orgs |\n\nFor B2B, ensure the user has an active org session. The `has()` check evaluates the active entity (user or org).\n\n## Checkout Flows\n\nClerk 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 />`:\n\n```typescript\n'use server'\nimport { redirect } from 'next/navigation'\n\nexport async function upgradeAction() {\n\tredirect('/pricing')\n}\n```\n\n## Error Signatures (diagnose fast)\n\nWhen 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.\n\n| Error / symptom | Root cause | Fix |\n|---|---|---|\n| `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`. |\n| `<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 |\n| 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* |\n| Can't find a Features page | Features are per-plan, not global | Dashboard → Billing → Plans → click plan → Features |\n| `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 |\n| `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 |\n| `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 |\n| 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(.*)'])` |\n\n## Billing Gates Permissions\n\nWhen 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.\n\n## See Also\n\n- `clerk-setup` - Initial Clerk install\n- `clerk-orgs` - B2B organizations (required for B2B billing and seat-limit plans)\n- `clerk-webhooks` - Webhook signature verification and routing\n"
}SHA-256: e088ecd8e08f924ac4d1dcda402be8c05c275423dc505984c3adb97380024ced