Generated banner for Clerk Billing
Generated logo for Clerk Billing

Clerk Billing

ClerkOfficialCreated by clerk

  • Skill
  • Tool integration
  • Security
  • MIT
  • No issues found

From the author

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.
— the description by clerk

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

  1. Download the skill as a ZIP:

    https://mp-staging.autogineer.com/agent-skills/clerk/clerk-billing/download
  2. Unzip 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 }), or has({ feature }) usage works. Two paths: (1) Dashboard → 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 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. 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

ActionURL
Enable Billinghttps://dashboard.clerk.com/last-active?path=billing/settings
Create / edit planshttps://dashboard.clerk.com/last-active?path=billing/plans
Membership mode (B2C + B2B coexistence)https://dashboard.clerk.com/last-active?path=organizations-settings
Edit featuresPlans → click a plan → Features section (no direct URL)

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 only
Pull current billing config
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:

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:

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?

TaskReference
<PricingTable /> props, <CheckoutButton />, <Show> billing patternsreferences/billing-components.md
B2C patterns (individual user subscriptions, Membership optional prerequisite)references/b2c-patterns.md
B2B patterns (org subscriptions, seat-limit plans, admin-gated billing UI)references/b2b-patterns.md
Webhook event catalog, payload shapes, handler templatesreferences/billing-webhooks.md

References

ReferenceDescription
references/billing-components.md<PricingTable /> and subscription UI
references/b2c-patterns.mdB2C subscription billing patterns
references/b2b-patterns.mdB2B billing with organization subscriptions and seat-limit plans
references/billing-webhooks.mdSubscription 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.

ScenarioCorrect check
Gate the "Export CSV" buttonhas({ feature: 'export' })
Gate the "Analytics" sectionhas({ feature: 'analytics' })
Gate all of /dashboard/prohas({ plan: 'pro' })
Check if org has team subscriptionhas({ plan: 'org:team' })
Gate SSO configurationhas({ 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 use has({ plan }) or has({ 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.canceled event. Cancellation fires at the item level as subscriptionItem.canceled.

IntentStripe event nameClerk event name
Subscription createdcustomer.subscription.createdsubscription.created
Subscription updatedcustomer.subscription.updatedsubscription.updated
Subscription active(none)subscription.active
Subscription past due(none)subscription.pastDue
Subscription item canceledcustomer.subscription.deletedsubscriptionItem.canceled
Subscription item past dueinvoice.payment_failedsubscriptionItem.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):

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:

TierPlan SlugExample Features
Free(no plan check needed)basic features
Starterstarteranalytics, api_access
Proproanalytics, export, team
Enterpriseenterpriseall features + sso, audit_logs

Use lowercase slugs matching what you define in the dashboard.

B2B vs B2C Billing

ScenarioWho subscribesPlan check
B2C SaaSIndividual userhas({ plan: 'pro' }) on user session
B2B SaaSOrganizationhas({ plan: 'org:team' }) on org session
Seat-limited B2BOrganizationPlan has a seat cap; pricing is per-plan, not per-member, tier your plans for bigger orgs

For B2B, ensure the user has an active org session. The has() check evaluates the active entity (user or org).

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 / symptomRoot causeFix
Clerk: 🔒 The <PricingTable/> component cannot be rendered when billing is disabled. (code: cannot_render_billing_disabled, dev only)Billing is not enabled for this instanceEnable Billing at dashboard.clerk.com → Billing → Settings, or run clerk enable billing.
<PricingTable /> renders emptyNo plans, OR plan in the wrong tab (User vs Organization), OR Billing not enabledCreate plan in matching tab; pass for="organization" for B2B; check Billing Settings
Users can't subscribe to a personal plan on a B2C + B2B appMembership required mode (default since 2025-08-22) disables personal accounts, signed-in users are forced into choose-organization and never land on a personal-subscription stateIf you need personal + org subscriptions coexisting: Dashboard → Organizations settings → Membership optional
Can't find a Features pageFeatures are per-plan, not globalDashboard → Billing → Plans → click plan → Features
has({ plan: 'pro' }) always returns false after a successful checkoutSession token hasn't been refreshed to include the new planawait clerk.session?.reload() or navigate to force a new session
has({ plan: 'pro' }) returns false before any subscribe attemptPlan slug mismatch (case-sensitive), OR Billing not enabled, OR payment gateway not connected in productionVerify slug in Dashboard → Billing → Plans; confirm Billing → Settings shows enabled + connected gateway
has({ permission: 'org:x:y' }) returns false for a user who has the roleThe Feature tied to that permission is not included in the organization's active PlanAdd the Feature to the Plan in Dashboard → Billing → Plans → Features
Webhook 401 or 404 / signature verification failedCLERK_WEBHOOK_SIGNING_SECRET mismatch, or auth.protect() covers the webhook routeCopy 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 install

  • clerk-orgs - B2B organizations (required for B2B billing and seat-limit plans)

  • clerk-webhooks - Webhook signature verification and routing

Files

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 the webhook route has no session check (proxy.ts stays a bare clerkMiddleware()).",
      "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_)",
        "Nothing enforces a session on /api/webhooks: no auth.protect() and no 401/404 based on auth(), in the handler or in proxy.ts/middleware.ts. No createRouteMatcher",
        "Rejects requests with an invalid signature before any processing"
      ]
    },
    {
      "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"
      ]
    }
  ]
}

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