AutoGineer summary
Written by AutoGineer from the skill’s own files, not by the author
Clerk Billing provides a comprehensive system for managing SaaS monetization, including subscription lifecycle management, plan gating, and checkout flows. It allows developers to render pricing tables and checkout drawers directly within their applications, configure B2C or B2B subscription plans, and enforce feature entitlements using server-side or client-side checks. The system integrates with Stripe for payment processing and supports programmatic configuration via the Clerk CLI.
What it does
- Renders pre-built pricing tables and in-app checkout drawers
- Configures subscription plans and feature entitlements
- Enforces feature gating and plan access via has() checks
- Manages B2B seat-limit plans for organizations
- Handles subscription lifecycle events via webhooks
- Supports programmatic billing configuration via CLI
When to use it
- When implementing subscription-based monetization for a SaaS application
- When you need to gate specific features or tiers based on user or organization plans
- When you require a managed checkout flow for B2C or B2B customers
- When you need to synchronize billing configuration across development and production environments
Advantages
- Reduces development time with pre-built UI components
- Simplifies complex B2B seat-limit management
- Provides unified authorization checks for both plans and features
- Enables version-controlled billing configuration through CLI tools
Usage examples
- “How do I gate a specific feature like analytics for my users?”
- Use the has({ feature: 'analytics' }) check in your server or client components to conditionally render the feature.
- “How can I render a pricing table for my B2B organization?”
- Use the PricingTable component with the for='organization' prop to display organization-level plans.
- “How do I enable billing for my application?”
- Enable billing via the Clerk Dashboard under Billing Settings or by running the clerk enable billing command.
- “How do I handle subscription updates in my database?”
- Implement a webhook handler that listens for Clerk billing events like subscription.updated and updates your local database records.
Install
Download the skill as a ZIP:
https://mp-staging.autogineer.com/agent-skills/clerk/clerk-billing/downloadUnzip it into the cross-client skills folder of your project:
mkdir -p .agents/skills && unzip clerk-billing.zip -d .agents/skills
Documentation by clerk
Billing
STOP, prerequisite. Billing must be enabled before any
<PricingTable />,<CheckoutButton />,has({ plan }), orhas({ feature })usage works. Two paths: (1) Dashboard → Billing → Settings, or (2)clerk enable billing(see "Agent-first: Programmatic billing config" below). Enabling auto-creates defaultfree_user/free_orgplans. 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/nextjsandclerk-jspackage versions. Seeclerkskill for the supported version table.
Quick Start
Enable Billing, via Dashboard → Billing → Settings or
clerk enable billing(see Agent-first section). Skipping this throwscannot_render_billing_disabledin dev and renders empty in prod.Create plans in the matching tab, Dashboard → Billing → Plans. Two tabs, slugs scoped per tab, not movable after creation:
User Plans →
<PricingTable />(defaultfor="user")Organization Plans →
<PricingTable for="organization" />
Wrong-tab is the #1 cause of an empty
<PricingTable />. Plans live in Clerk; not synced to Stripe.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.Render
<PricingTable />(passfor="organization"for B2B).Gate access with
has({ plan })orhas({ feature })fromauth().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-cli). Billing is account-only — unlike orgs, it cannot be enabled on an unclaimed app; claim the app first.
Enable Billing via CLI
clerk enable billing # both targets (default, auto-creates free_user + free_org plans)
clerk enable billing --for org # org only
clerk enable billing --for user # user onlyPull current billing config
clerk config pull --keys billing > billing.jsonThis 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:
clerk config patch --file billing.json --dry-run
clerk config patch --file billing.jsonPass --instance prod to target the production instance instead of dev.
Raw PATCH (full control)
For one-shot plan/feature updates without a config file:
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, seeclerk-webhooksskill for the lifecycle events.Top-level
featuresmap 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
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
With Cache Components (cacheComponents: true, the create-next-app default), request data outside <Suspense> fails next build. Wrap an async auth() child in <Suspense> when rendering data; add a sibling loading.tsx when auth() gates a page with redirect(). See clerk-nextjs-patterns and Read auth data inside Suspense.
1. Render the Pricing Table
Show available plans to users with a single component:
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:
import { auth } from '@clerk/nextjs/server'
import { Suspense } from 'react'
async function Analytics() {
const { has } = await auth()
const canViewAnalytics = has({ feature: 'analytics' })
const canExport = has({ feature: 'export' })
return (
<div>
{canViewAnalytics && <AnalyticsChart />}
{canExport && <ExportButton />}
</div>
)
}
export default function AnalyticsPage() {
return (
<Suspense>
<Analytics />
</Suspense>
)
}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:
'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):
// app/pro/page.tsx
import { auth } from '@clerk/nextjs/server'
import { redirect } from 'next/navigation'
export default async function ProDashboard() {
const { has } = await auth()
if (!has({ plan: 'pro' })) {
redirect('/pricing')
}
return <ProFeatures />
}Add app/pro/loading.tsx next to it, such as export default function Loading() { return <p>Loading...</p> }.
5. Client-Side Plan Checks
Use useAuth() hook for client components:
'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:
import { auth } from '@clerk/nextjs/server'
import { Suspense } from 'react'
async function CurrentPlan() {
const { has } = await auth()
const currentPlan = has({ plan: 'pro' })
? 'pro'
: has({ plan: 'starter' })
? 'starter'
: 'free'
return (
<div>
<h2>Current Plan</h2>
<p>You are on the {currentPlan} plan</p>
{currentPlan === 'free' && <a href="/pricing">Upgrade</a>}
</div>
)
}
export default function AccountPage() {
return (
<Suspense>
<CurrentPlan />
</Suspense>
)
}Client component, full subscription details via useSubscription():
'use client'
import { useSubscription } from '@clerk/nextjs/experimental'
export function SubscriptionDetails() {
const { data: subscription, isLoading } = useSubscription()
if (isLoading) return null
if (!subscription) return <a href="/pricing">Choose a plan</a>
return (
<div>
<p>Status: {subscription.status}</p>
{subscription.nextPayment && (
<p>Next payment: {subscription.nextPayment.date.toLocaleDateString()}</p>
)}
</div>
)
}
useSubscription()is for display only. For authorization checks (gating content or routes), always usehas({ plan })orhas({ feature }).
8. Protect API Routes by Plan
Gate API routes using auth():
import { auth } from '@clerk/nextjs/server'
import { NextResponse } from 'next/server'
export async function GET() {
const { has } = await auth()
if (!has({ plan: 'pro' })) {
return NextResponse.json({ error: 'Pro plan required' }, { status: 403 })
}
return NextResponse.json({ data: 'premium data' })
}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.canceledevent. Cancellation fires at the item level assubscriptionItem.canceled.
Intent Stripe event name Clerk event name Subscription created customer.subscription.createdsubscription.createdSubscription updated customer.subscription.updatedsubscription.updatedSubscription active (none) subscription.activeSubscription past due (none) subscription.pastDueSubscription item canceled customer.subscription.deletedsubscriptionItem.canceledSubscription item past due invoice.payment_failedsubscriptionItem.pastDueSubscription item updated (none) subscriptionItem.updatedSubscription item active (none) subscriptionItem.activeSubscription item upcoming renewal (none) subscriptionItem.upcomingSubscription item ended (none) subscriptionItem.endedSubscription item abandoned (none) subscriptionItem.abandonedSubscription item expired (none) subscriptionItem.expiredSubscription item incomplete (none) subscriptionItem.incompleteFree trial ending soon (none) subscriptionItem.freeTrialEndingPayment attempt created (none) paymentAttempt.createdPayment attempt updated (none) paymentAttempt.updatedAlways use Clerk's event names, never Stripe's, in
evt.typechecks.
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 underevt.data.items[i].plan.slug. The subscription id is simplyevt.data.id. Subscription items do not carry asubscription_idfield back-reference, so insubscriptionItem.*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):
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 by rendering <PricingTable /> on a billing page. It renders differently for subscribed users: it shows the current plan and allows upgrades or cancellations, all through Clerk's in-app checkout drawer. For a full billing page that changes its copy by plan, see references/billing-components.md.
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 />:
'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, 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 or 404 / signature verification failed | CLERK_WEBHOOK_SIGNING_SECRET mismatch, or auth.protect() covers the webhook route | Copy the Signing Secret from Dashboard → Webhooks. Ensure nothing calls auth.protect() for /api/webhooks(.*), in middleware or in the handler |
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 installclerk-orgs- B2B organizations (required for B2B billing and seat-limit plans)clerk-webhooks- Webhook signature verification and routing
Files
- ATTRIBUTION.mdAdded by AutoGineer833 B
- SKILL.md20.5 KiB
- evals/evals.json15.1 KiB
- references/b2b-patterns.md6.0 KiB
- references/b2c-patterns.md5.3 KiB
- references/billing-components.md5.9 KiB
- references/billing-webhooks.md7.5 KiB
ATTRIBUTION.md
Attribution
This folder is an unmodified copy of the Agent Skill "clerk-billing", distributed by AutoGineer (https://autogineer.com/agent-skills).
Source: https://github.com/clerk/skills/tree/c5d2c0d6c5b63243146b4893c875f6ebef55977c/skills/clerk-billing
Commit: c5d2c0d6c5b63243146b4893c875f6ebef55977c
Created by: clerk (https://github.com/clerk/skills/tree/c5d2c0d6c5b63243146b4893c875f6ebef55977c/skills/clerk-billing)
Publisher: Clerk (https://github.com/clerk)
Licence: MIT License (https://spdx.org/licenses/MIT.html)
Licence evidence: declared in the frontmatter ("license: MIT")
Retrieved: 2026-11-07
The skill's own files are unmodified copies. AutoGineer is not affiliated with or endorsed by the author or the publisher.
Files added by AutoGineer
ATTRIBUTION.md: this file, written by AutoGineer
Requirements and compatibility
Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta
“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.
Needs: Declares allowed tools
The author says: 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.
Security
No issues found by our static scan· No issues found
A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.
Attribution and licence
- Created by
- clerk
- Publisher
- Clerk (Official)
- Source
- clerk/skills · skills/clerk-billing · commit c5d2c0d
- Licence
- MIT License (redistribution allowed)
- ATTRIBUTION.md (written by AutoGineer)
- Synced
AutoGineer is not affiliated with or endorsed by clerk.
Download ZIP
The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download clerk-billing.zip