Documentation

Webhooks Stripe

Events handlés, configuration, mapping email et idempotence.

Le webhook Stripe vit sous /api/billing/webhook. Il met à jour Subscription en DB et déclenche les emails transactionnels.

Events handlés

EventEffet
checkout.session.completedCrée/active la Subscription, audit subscription.created
customer.subscription.updatedSync DB sur changement de plan (Portal Stripe ou flow upgrade), cancel_at_period_end, status
invoice.payment_succeededEmail payment-succeeded, met à jour currentPeriodEnd
invoice.payment_failedEmail payment-failed
invoice.upcomingEmail subscription-renewing (7 jours avant)
customer.subscription.trial_will_endEmail subscription-trial-ending (3 jours avant)
customer.subscription.deletedMarque Subscription.status = "canceled", email subscription-ended

Tous les autres events sont ignorés silencieusement (retour 200).

Mapping email

Avant chaque email, loadEmailContext(stripeCustomerId, stripePriceId) retrouve le user, la locale, et le nom localisé du plan via Plan.stripePriceIdMonthly ou Plan.stripePriceIdYearly. Si aucun plan ne matche, fallback sur "Premium".

Les emails partent dans la locale du user (User.locale), pas celle du browser au moment de l'event.

Configuration côté Stripe

Dans le Dashboard Stripe → Developers → Webhooks :

  1. Endpoint URL : https://ton-domaine.com/api/billing/webhook
  2. Events à écouter :
    • checkout.session.completed
    • customer.subscription.updated
    • invoice.payment_succeeded
    • invoice.payment_failed
    • invoice.upcoming
    • customer.subscription.trial_will_end
    • customer.subscription.deleted
  3. Copie le Signing secret (commence par whsec_) dans .env :
STRIPE_WEBHOOK_SECRET=whsec_...

En local, utilise la CLI Stripe :

stripe listen --forward-to localhost:3000/api/billing/webhook

Elle te donne un whsec_ de dev à mettre dans .env.

Variables d'env requises

STRIPE_SECRET_KEY=sk_live_...      # ou sk_test_ en dev
STRIPE_PUBLISHABLE_KEY=pk_...
STRIPE_WEBHOOK_SECRET=whsec_...

Plus côté plans : les stripePriceIdMonthly / stripePriceIdYearly remplis dans /admin/plans (voir Plans).

Idempotence — TODO en prod

Le boilerplate ne dédoublonne pas les events Stripe par event.id. Stripe peut renvoyer le même event en cas de timeout. En pratique, le modèle Subscription utilise des upserts par stripeCustomerId, donc ce n'est pas catastrophique — mais les emails partent deux fois.

Pour rendre le webhook strictement idempotent, ajoute :

model StripeEvent {
  id          String   @id              // event.id Stripe
  type        String
  processedAt DateTime @default(now())
}

Et dans le handler, en tête :

try {
  await prisma.stripeEvent.create({ data: { id: event.id, type: event.type } });
} catch {
  return NextResponse.json({ received: true }); // déjà traité
}

Allez plus loin

On this page