Start here

Arrows to move, enter to open, escape to close.

Ship/Now

Ce qui est branché · 3 min

Paiements

Stripe Checkout, un webhook idempotent, et pourquoi le prix ne vient jamais du navigateur.

L'argent bouge à deux endroits et nulle part ailleurs : une route qui ouvre une session Checkout, et un webhook qui décide qu'une vente a eu lieu.

Ouvrir le paiement

src/app/api/checkout/route.ts prend un identifiant d'offre. C'est tout ce qu'il prend.

const { data: plan } = await admin
  .from("plans").select("*").eq("id", planId).single();

const session = await stripe().checkout.sessions.create({
  mode: "payment",
  line_items: [{ quantity: 1, price_data: {
    currency: plan.currency,
    unit_amount: plan.price_cents,
    tax_behavior: "exclusive",
    product_data: { name: plan.name, tax_code: "txcd_10000000" },
  }}],
  automatic_tax: { enabled: true },
  metadata: { plan_id: plan.id, user_id: user?.id ?? "" },
  success_url: `${origin}/app?welcome=1`,
  cancel_url: `${origin}/#pricing`,
});

Le prix est lu dans la base. Il n'est jamais pris dans le corps de la requête, parce qu'un montant envoyé est une remise envoyée. Si votre route de paiement accepte amount, quelqu'un finira par lui envoyer un.

Le webhook fait seul autorité

checkout.session.completed est l'endroit où une commande devient réelle. Trois règles la rendent sûre à rejouer, ce qui compte parce que Stripe réessaie et livre parfois le même événement plusieurs fois :

Vérifier la signature en premier

Un POST non signé vers cette URL, c'est quelqu'un qui se fait passer pour Stripe. Refusez-le avec un 400 avant de lire le moindre champ.

Indexer la ligne sur l'identifiant de session

Un upsert sur stripe_session_id, qui est unique. Un rejeu met à jour la ligne déjà écrite au lieu de créer une seconde commande payée.

Vérifier avant de délivrer

Cherchez une licence ou un droit déjà associé à cette commande et sortez tôt s'il existe. Sinon un réessai envoie un second e-mail de bienvenue, et c'est ainsi que vos clients découvrent que votre webhook est cassé.

Les remboursements

Activez charge.refunded sur l'endpoint et défaites tout ce que la vente avait créé : passez la commande en remboursée, retirez le droit d'accès. Si vos conditions disent qu'un remboursement annule quelque chose, quelque chose doit réellement l'annuler.

La TVA

Pour un produit numérique vendu depuis l'UE, activez Stripe Tax et ajoutez au minimum une immatriculation pour votre propre pays. Deux réglages décident de ce que voit l'acheteur :

  • tax_code: "txcd_10000000" marque la ligne comme service électronique général. Sans lui, Stripe ne peut pas choisir de taux et ne facture rien.
  • tax_behavior: "exclusive" signifie que votre montant est hors taxes et que la taxe s'ajoute. inclusive signifie qu'il la contient déjà.

Tester

Carte 4242 4242 4242 4242, n'importe quelle date future, n'importe quel CVC. Pour exercer le webhook en local, redirigez les événements plutôt que de deviner la charge utile :

stripe listen --forward-to localhost:3000/api/webhooks/stripe

La commande affiche un whsec_... pour la session. Mettez-le dans .env le temps de vos tests.

Une erreur ou un manque sur cette page ? Dites-le-nous.