Le build tombe sur une erreur de types dans lib/stripe.ts. La ligne est celle-ci :
Type '"2025-06-30.basil"' is not assignable to type '"2025-07-30.basil"'.
Je n'ai touché à rien du côté des paiements. Ce qui s'est passé, c'est que bun update a ramené une nouvelle version du SDK de Stripe, et le paquet embarque un littéral avec la version d'API qu'il supporte. Le jour où ce littéral change, tout ce qui passe apiVersion explicitement cesse de compiler. Ça m'est arrivé cinq fois en un peu plus d'un an, sur quatre projets différents.
La première fois, c'était un incident. À la cinquième, c'était déjà quelque chose que j'attendais.
Le même commit, dans quatre dépôts, à un an d'écart
La séquence est écrite dans l'historique et elle est presque comique à force de répétition. 2025-06-30.basil vers 2025-07-30.basil en août. Un mois plus tard, 2025-07-30.basil vers 2025-08-27.basil, dans deux dépôts le même jour. Des mois après, 2025-11-17.clover vers 2026-01-28.clover dans un SaaS de génération d'images. Et peu après, encore une fois, dans le boilerplate que j'utilise pour démarrer mes projets.
Les messages de commit sont identiques : « update Stripe API version to X ». Le diff aussi. Et c'est là la première leçon, qui n'a rien à voir avec Stripe : la même chaîne était répétée à quatre endroits du même dépôt. Une dans le webhook, et trois autres dans createCheckout, createCustomerPortal et findCheckoutSession, chacune avec son propre new Stripe(secretKey, { apiVersion, typescript: true }). Quatre lignes identiques à changer à la main à chaque fois, et quatre endroits où en oublier une.
J'ai eu le culot de laisser un commentaire à côté de chacune :
const stripe = new Stripe(secretKey, {
apiVersion: "2026-01-28.clover", // TODO: update this when Stripe updates their API
typescript: true,
});
Un TODO répété quatre fois, c'est la preuve écrite que vous savez que vous allez refaire le travail et que vous ne l'avez quand même pas centralisé.
Les dommages collatéraux ne sont pas dans la version, ils sont dans les types
La partie qui casse le build est triviale : vous changez la chaîne et ça compile. Ce qui coûte vraiment, ce sont les changements de forme cachés dans la même montée de version, parce que le SDK réécrit ses types en même temps.
Le cas concret qui m'a mangé un après-midi, c'était le webhook d'invoice.paid. Je sortais le price ID comme ça :
const priceIdIP = stripeIP.lines.data[0].pricing?.price_details?.price;
Avec la nouvelle version, ce champ devient string | Price | undefined selon qu'il arrive expansé ou non. TypeScript n'avale plus la comparaison avec p.priceId et, plus grave, le runtime se met à comparer un objet à une chaîne : le find sur la table des prix ne trouve rien, tombe dans le if (!pricingIP || !priceIdIP) et le paiement n'est pas appliqué en base de données. L'encaissement est fait chez Stripe et l'utilisateur n'a pas ses crédits.
Le premier correctif a été l'évident :
const rawPrice = stripeIP.lines.data[0].pricing?.price_details?.price;
const priceIdIP = typeof rawPrice === "string" ? rawPrice : rawPrice?.id;
Et j'ai dû y revenir le jour même, parce que rawPrice?.id ne restreignait toujours pas bien le type sur tous les chemins et compilait mal selon le mode de checkout. La version qui est restée est plus laide et plus honnête :
const priceIdIP =
typeof rawPrice === "string"
? rawPrice
: typeof rawPrice === "object" && rawPrice
? (rawPrice as { id: string }).id
: undefined;
Pareil avec les remises. discounts: couponId ? [{ coupon: couponId }] : [] cohabite avec allow_promotion_codes: true, et il y a là une interaction que Stripe ne pardonne pas : si vous passez un coupon fixe, les codes promotionnels du client cessent de s'appliquer. Deux commits le même jour pour faire ça correctement. Aucun des deux n'était dû à la version : ils étaient dus au fait d'avoir touché au checkout.
Pourquoi j'ai fini par ne plus figer la version
Après le cinquième bump, j'ai fait l'inverse de ce que dit l'orthodoxie. J'ai supprimé l'apiVersion aux quatre endroits :
const stripe = new Stripe(secretKey);
Sans apiVersion, le SDK utilise la version qu'il a compilée à l'intérieur. Et c'est exactement la version contre laquelle sont générés les types TypeScript que vous utilisez. Le décalage qui cassait le build disparaît par construction : vous ne pouvez pas avoir le SDK dans une version et le littéral dans une autre s'il n'y a qu'une seule source.
L'argument classique pour la figer, c'est que vous contrôlez ainsi le moment où le comportement change. C'est vrai, mais ça cesse de l'être quand vous figez la version et en même temps mettez à jour le SDK tous les mois. Là, vous n'avez pas deux leviers : vous avez un levier et une chaîne que vous synchronisez à la main dans quatre fichiers. Le vrai pin, c'est le lockfile, pas le littéral.
Si vous voulez réellement figer la version d'API, le pin doit être à un seul endroit, exporté, et le SDK doit être cloué dans le package.json avec une version exacte, pas avec un ^. Figer l'un en laissant l'autre flotter, c'est le pire des deux mondes.
La checklist quand une nouvelle version sort
- Mettre à jour le SDK et le littéral en même temps, jamais séparément. S'ils vont séparément, l'erreur de types apparaît dans une PR qui ne parle pas de paiements.
- Chercher dans tout le dépôt, pas dans le fichier évident. Cinq fois j'ai trouvé une cinquième occurrence de
new Stripe(à un endroit dont je ne me souvenais pas. - Relire les handlers de webhook avant de valider un build au vert. Le compilateur vous prévient pour l'
apiVersion, mais un champ qui passe destringà objet peut compiler et échouer à l'exécution s'il y a un?.au milieu. - Tester
invoice.paidavec le CLI, pas seulementcheckout.session.completed. Les événements récurrents empruntent un autre chemin de données et cassent sans que personne ne s'en aperçoive avant le mois suivant. - Vérifier le coupon et le code promotionnel ensemble si la session les embarque.
Mon erreur a été de traiter chaque bump comme une interruption. Ça n'en est pas une : une version sort tous les deux ou trois mois et ça coûte une demi-heure si vous pensez à regarder les webhooks, un après-midi sinon. Ça, c'est de la maintenance, et la maintenance se planifie. Ce qui ne se planifie pas se paie sous la forme d'un encaissement qui est entré chez Stripe et n'est jamais arrivé en base de données.
