clerk-billing

Clerk Billing pour la gestion des abonnements - afficher le `PricingTable` de Clerk

npx skills add https://github.com/clerk/skills --skill clerk-billing

Facturation

STOP, condition préalable réservée au Dashboard. La facturation doit être activée à partir du Dashboard Clerk avant que tout usage de <PricingTable />, <CheckoutButton />, has({ plan }), ou has({ feature }) fonctionne. Le CLI Clerk et l'API Backend n'exposent pas de bouton pour cela actuellement, le seul chemin est dashboard.clerk.com → votre app → Billing → Settings. Les instances de développement peuvent utiliser la passerelle de développement Clerk partagée (aucun compte Stripe nécessaire) ; la production nécessite un compte Stripe pour le traitement des paiements uniquement.

Note : Les APIs de facturation sont encore expérimentales. Épinglez vos versions de paquets @clerk/nextjs et clerk-js. Voir clerk skill pour le tableau des versions supportées.

Démarrage rapide

  1. Activer la facturation, Dashboard → Billing → Settings. Réservé au Dashboard ; aucun chemin CLI ou API. Sauter cette étape génère cannot_render_billing_disabled en développement et affiche rien en production.

  2. Créer des plans dans l'onglet correspondant, Dashboard → Billing → Plans. Deux onglets, slugs limités par onglet, non déplaçables après création :

    • User Plans<PricingTable /> (for="user" par défaut)
    • Organization Plans<PricingTable for="organization" />

    Un plan au mauvais onglet est la cause #1 d'un <PricingTable /> vide. Les plans vivent dans Clerk ; ils ne sont pas synchronisés avec Stripe.

  3. Ajouter des features dans un plan, ouvrir le plan dans Dashboard → Billing → Plans, utiliser sa section Features. Les features sont limitées par plan, pas globales. Le même slug peut s'attacher à plusieurs plans ; has({ feature: 'export' }) correspond si le plan actif contient ce slug.

  4. Afficher <PricingTable /> (passer for="organization" pour B2B).

  5. Contrôler l'accès avec has({ plan }) ou has({ feature }) depuis auth().

  6. Gérer les webhooks de facturation pour le cycle de vie des abonnements.

Raccourcis du Dashboard

Action URL
Activer la facturation https://dashboard.clerk.com/last-active?path=billing/settings
Créer / modifier les plans https://dashboard.clerk.com/last-active?path=billing/plans
Mode d'adhésion (coexistence B2C + B2B) https://dashboard.clerk.com/last-active?path=organizations-settings
Modifier les features Plans → cliquer sur un plan → section Features (pas d'URL directe)

De quoi avez-vous besoin ?

Tâche Référence
Props de <PricingTable />, <CheckoutButton />, modèles <Show> de facturation references/billing-components.md
Modèles B2C (abonnements utilisateur individuels, condition préalable Membership optional) references/b2c-patterns.md
Modèles B2B (abonnements org, plans avec limite de sièges, interface de facturation restreinte aux admins) references/b2b-patterns.md
Catalogue d'événements webhook, formes de payloads, modèles de handler references/billing-webhooks.md

Références

Référence Description
references/billing-components.md <PricingTable /> et interface d'abonnement
references/b2c-patterns.md Modèles de facturation d'abonnement B2C
references/b2b-patterns.md Facturation B2B avec abonnements d'organisation et plans avec limite de sièges
references/billing-webhooks.md Gestion d'événements du cycle de vie d'abonnement

Documentation

Features vs Plans : Quand utiliser lequel

Utilisez has({ feature: 'slug' }) pour contrôler une capacité spécifique, export, analytique, accès API, journaux d'audit.

Utilisez has({ plan: 'slug' }) pour contrôler un tier, afficher le dashboard pro, vérifier le niveau d'abonnement org, rediriger les utilisateurs gratuits.

Scénario Vérification correcte
Contrôler le bouton "Export CSV" has({ feature: 'export' })
Contrôler la section "Analytique" has({ feature: 'analytics' })
Contrôler tout /dashboard/pro has({ plan: 'pro' })
Vérifier si l'org a un abonnement team has({ plan: 'org:team' })
Contrôler la configuration SSO has({ feature: 'sso' })

Quand un utilisateur dit « contrôler la feature export » ou « contrôler analytique », utilisez toujours has({ feature }). N'utilisez has({ plan }) que quand le contrôle concerne le tier du plan lui-même, pas une capacité spécifique.

Modèles clés

1. Afficher la table de tarification

Montrer les plans disponibles aux utilisateurs avec un seul composant :

import { PricingTable } from '@clerk/nextjs'

export default function PricingPage() {
    return (
        <main>
            <h1>Choisir un plan</h1>
            <PricingTable />
        </main>
    )
}

<PricingTable /> affiche automatiquement tous les plans configurés dans le Dashboard Clerk. Sélectionner un plan ouvre le tiroir de checkout in-app de Clerk. Aucune prop nécessaire pour une utilisation basique. Pour B2B, passez for="organization" pour afficher les plans au niveau org au lieu des plans utilisateur.

2. Vérifier les droits aux features (côté serveur)

Contrôler par features individuelles, c'est l'approche préférée pour les capacités spécifiques :

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>
    )
}

Les features sont configurées dans Dashboard Clerk → Billing → Features et assignées aux plans. Utilisez has({ feature }) au lieu de has({ plan }) pour contrôler les capacités granulaires, vérifiez la feature, pas le plan.

3. Vérifier les droits aux features (côté client)

Utilisez useAuth() pour le contrôle des features côté client. Combinez avec les vérifications côté serveur pour une couverture complète :

'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>
    )
}

Les Server Components utilisent auth(), les Client Components utilisent useAuth(). Les deux supportent has({ feature }) et has({ plan }).

4. Vérifier le plan d'abonnement côté serveur

Contrôler l'accès par plan d'abonnement (utilisez ceci pour les contrôles au niveau tier, pas pour les features individuelles) :

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. Vérifications du plan côté client

Utilisez le hook useAuth() pour les composants client :

'use client'
import { useAuth } from '@clerk/nextjs'

export function UpgradePrompt() {
    const { has } = useAuth()

    if (has?.({ plan: 'pro' })) {
        return null
    }

    return (
        <div>
            <p>Passez à Pro pour accéder à cette feature</p>
            <a href="/pricing">Voir les plans</a>
        </div>
    )
}

6. Facturation basée sur les sièges B2B avec organisations

Les plans org peuvent avoir une limite de sièges (plafond d'adhésion) que Clerk applique au moment de l'invitation. Utilisez le préfixe de slug org: pour les vérifications de plan côté org (p. ex. has({ plan: 'org:team' })) pour maintenir le contrôle sans ambiguïté. Afficher la page de tarification B2B avec <PricingTable for="organization" />, et utiliser <OrganizationProfile /> pour l'interface de facturation du compte org.

Voir references/b2b-patterns.md pour la nomenclature des plans en tiers, les invariants de limite de sièges, la facturation réservée aux admins, et les handlers de webhooks.

7. Afficher l'état de l'abonnement

Vérifier les plans spécifiques avec has({ plan }), ou utiliser useSubscription() pour les détails d'abonnement complets dans les composants client. Ne lisez pas les informations de plan directement depuis sessionClaims, ce n'est pas le chemin supporté.

Composant serveur, vérifier les plans spécifiques :

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>Plan actuel</h2>
            <p>Vous êtes sur le plan {currentPlan}</p>
            {currentPlan === 'free' && <a href="/pricing">Passer à la version payante</a>}
        </div>
    )
}

Composant client, détails complets d'abonnement 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">Choisir un plan</a>

    return (
        <div>
            <p>Statut : {subscription.status}</p>
            {subscription.nextPayment && (
                <p>Prochain paiement : {subscription.nextPayment.date.toLocaleDateString()}</p>
            )}
        </div>
    )
}

useSubscription() est pour l'affichage uniquement. Pour les vérifications d'autorisation (contrôler le contenu ou les routes), utilisez toujours has({ plan }) ou has({ feature }).

8. Protéger les routes API par plan

Contrôler les routes API en utilisant 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: 'Plan Pro requis' }, { status: 403 })
    }

    return NextResponse.json({ data: 'données premium' })
}

9. Gérer les webhooks de facturation

Les noms d'événements Clerk diffèrent des noms d'événements Stripe. Les webhooks de facturation Clerk utilisent la notation pointée et camelCase, pas le format underscore de Stripe.

Il n'y a pas d'événement subscription.canceled. L'annulation se déclenche au niveau de l'item comme subscriptionItem.canceled.

Intention Nom d'événement Stripe Nom d'événement Clerk
Abonnement créé customer.subscription.created subscription.created
Abonnement mis à jour customer.subscription.updated subscription.updated
Abonnement actif (aucun) subscription.active
Abonnement en retard (aucun) subscription.pastDue
Item d'abonnement annulé customer.subscription.deleted subscriptionItem.canceled
Item d'abonnement en retard invoice.payment_failed subscriptionItem.pastDue
Item d'abonnement mis à jour (aucun) subscriptionItem.updated
Item d'abonnement actif (aucun) subscriptionItem.active
Renouvellement à venir d'item d'abonnement (aucun) subscriptionItem.upcoming
Item d'abonnement terminé (aucun) subscriptionItem.ended
Item d'abonnement abandonné (aucun) subscriptionItem.abandoned
Item d'abonnement expiré (aucun) subscriptionItem.expired
Item d'abonnement incomplet (aucun) subscriptionItem.incomplete
Fin du trial gratuit imminente (aucun) subscriptionItem.freeTrialEnding
Tentative de paiement créée (aucun) paymentAttempt.created
Tentative de paiement mise à jour (aucun) paymentAttempt.updated

Utilisez toujours les noms d'événements de Clerk, jamais ceux de Stripe, dans les vérifications evt.type.

Forme du payload. Les payloads de webhooks de facturation Clerk sont imbriqués. L'entité qui s'abonne vit sous evt.data.payer (champs : user_id?, organization_id?). Les infos de plan sont sur chaque item sous evt.data.items[i].plan.slug. L'id d'abonnement est simplement evt.data.id. Les items d'abonnement ne portent pas de champ back-référence subscription_id, donc dans les handlers subscriptionItem.* vous identifiez le record par l'id de l'item (evt.data.id) ou cherchez par payer plus plan.

Handler minimal pour établir le modèle (importer depuis @clerk/nextjs/webhooks, vérifier, créer des branches selon le nom d'événement Clerk) :

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('Vérification échouée', { 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 },
        })
    }

    // Ajouter plus de branches selon le catalogue d'événements ci-dessus (subscription.updated,
    // subscriptionItem.canceled, subscriptionItem.pastDue, etc.)

    return new Response('OK', { status: 200 })
}

Pour le modèle complet couvrant les 15 événements, les déclarations de type TS depuis @clerk/backend, la configuration de route publique proxy.ts, et le tableau des valeurs de statut d'abonnement, voir references/billing-webhooks.md.

10. Flux de passage à la version supérieure / inférieure

Laisser les utilisateurs gérer leur abonnement depuis l'intérieur de l'app :

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>Facturation</h1>
            {isPro ? (
                <div>
                    <p>Vous êtes sur le plan Pro</p>
                    <PricingTable />
                </div>
            ) : (
                <div>
                    <p>Passez à la version payante pour accéder aux features premium</p>
                    <PricingTable />
                </div>
            )}
        </div>
    )
}

<PricingTable /> s'affiche différemment pour les utilisateurs abonnés, il affiche le plan actuel et permet les passages à la version supérieure ou les annulations, le tout via le tiroir de checkout in-app de Clerk.

Nomenclature des plans et features

Les slugs de plan et les slugs de feature sont définis dans Dashboard Clerk → Billing. Conventions courantes :

Tier Plan Slug Exemples de features
Gratuit (aucune vérification de plan nécessaire) features basiques
Starter starter analytics, api_access
Pro pro analytics, export, team
Enterprise enterprise toutes les features + sso, audit_logs

Utilisez des slugs en minuscules correspondant à ce que vous définissez dans le dashboard.

Facturation B2B vs B2C

Scénario Qui s'abonne Vérification de plan
B2C SaaS Utilisateur individuel has({ plan: 'pro' }) sur la session utilisateur
B2B SaaS Organisation has({ plan: 'org:team' }) sur la session org
B2B avec limite de sièges Organisation Le plan a un plafond de sièges ; le tarif est par plan, pas par membre, étagez vos plans pour les plus grandes orgs

Pour B2B, assurez-vous que l'utilisateur a une session org active. La vérification has() évalue l'entité active (utilisateur ou org).

Flux de checkout

Clerk affiche son propre tiroir de checkout automatiquement via <PricingTable /> et <CheckoutButton />. Les plans et tarifs vivent dans Clerk. Pour déclencher le checkout depuis une server action, rediriger vers une page qui affiche <PricingTable /> :

'use server'
import { redirect } from 'next/navigation'

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

Signatures d'erreurs (diagnostiquer rapidement)

Quand vous voyez l'une de ces erreurs ou symptômes, la correction est presque toujours un bouton Dashboard, pas une modification de code. Ne commencez pas à modifier des composants.

Erreur / symptôme Cause racine Correction
Clerk: 🔒 The <PricingTable/> component cannot be rendered when billing is disabled. (code : cannot_render_billing_disabled, dev seulement) La facturation n'est pas activée pour cette instance Activer la facturation sur dashboard.clerk.com → Billing → Settings. Aucun chemin CLI.
<PricingTable /> s'affiche vide Aucun plan, OU plan au mauvais onglet (User vs Organization), OU facturation non activée Créer le plan dans l'onglet correspondant ; passer for="organization" pour B2B ; vérifier les paramètres de facturation
Les utilisateurs ne peuvent pas s'abonner à un plan personnel sur une app B2C + B2B Mode de membership requis (par défaut depuis 2025-08-22) désactive les comptes personnels, les utilisateurs connectés sont forcés dans choose-organization et n'accèdent jamais à un état d'abonnement personnel Si vous avez besoin que les abonnements personnels + org coexistent : Dashboard → Paramètres des organisations → Membership optional
Impossible de trouver une page Features Les features sont par plan, pas globales Dashboard → Billing → Plans → cliquer sur le plan → Features
has({ plan: 'pro' }) retourne toujours false après un checkout réussi Le token de session n'a pas été actualisé pour inclure le nouveau plan await clerk.session?.reload() ou naviguer pour forcer une nouvelle session
has({ plan: 'pro' }) retourne false avant toute tentative d'abonnement Slug de plan mal assorti (sensible à la casse), OU facturation non activée, OU passerelle de paiement non connectée en production Vérifier le slug dans Dashboard → Billing → Plans ; confirmer que Billing → Settings affiche activé + passerelle connectée
has({ permission: 'org:x:y' }) retourne false pour un utilisateur qui a le rôle La Feature liée à cette permission n'est pas incluse dans le Plan actif de l'organisation Ajouter la Feature au Plan dans Dashboard → Billing → Plans → Features
Webhook 401 / vérification de signature échouée Décalage de CLERK_WEBHOOK_SIGNING_SECRET ou route protégée par middleware Copier le Secret de signature depuis Dashboard → Webhooks ; ajouter la route webhook à createRouteMatcher(['/api/webhooks(.*)'])

Permissions de contrôles de facturation

Quand la facturation est activée, has({ permission: 'org:posts:edit' }) retourne false si la Feature associée à cette permission n'est pas incluse dans le Plan actif de l'organisation, même si l'utilisateur a la permission assignée via son rôle. C'est voulu : la facturation contrôle les permissions au niveau des features. Assurez-vous toujours que la Feature requise est attachée au Plan dans Dashboard → Billing → Plans → Features.

Voir aussi

  • clerk-setup - Installation initiale de Clerk
  • clerk-orgs - Organisations B2B (requis pour la facturation B2B et les plans avec limite de sièges)
  • clerk-webhooks - Vérification des signatures de webhooks et routage

Skills similaires