Polar.sh Payments in Node.js: Guide Complète Backend
Mis à jour le 3 janvier 2026
Intégration de paiement Polar.sh avec backend Node.js
Cet article développe mon fil X sur les paiements Polar.sh avec des exemples de code complets, la gestion des erreurs et les leçons apprises d’une utilisation en production.
Si vous construisez un SaaS ou vendez des produits numériques, vous avez besoin d’un fournisseur de paiement. Polar.sh est devenu mon choix pour les projets indépendants : voici comment l’intégrer avec un backend Node.js.
Pourquoi Polar.sh ? #why-polar-sh
Avant de plonger dans le code, voici pourquoi j’ai choisi Polar.sh plutôt que d’autres alternatives :
vs Stripe : Polar gère plus de choses nativement. Vous obtenez des pages de paiement hébergées, un portail client et la gestion du cycle de vie des abonnements sans avoir à les construire vous-même. Stripe vous donne plus de contrôle mais nécessite plus de code.
vs Paddle/Lemon Squeezy : Avantages similaires de marchand enregistré (ils gèrent la TVA/les taxes de vente), mais Polar est spécifiquement conçu pour les développeurs et les projets open-source. L’expérience développeur (DX) est nettement meilleure.
Ce que Polar gère pour vous :
- Pages de paiement hébergées
- Traitement des paiements
- Portail client
- Cycle de vie des abonnements
- Conformité fiscale (en tant que marchand enregistré)
Ce que vous gérez :
- Événements de webhook
- Droits d’accès des utilisateurs
- Suivi de l’utilisation
Configuration du projet #project-setup
Installez le SDK et la bibliothèque de vérification des webhooks :
npm install @polar-sh/sdk standardwebhooks
Initialisez le client Polar :
import { Polar } from '@polar-sh/sdk';
export const polar = new Polar({
accessToken: process.env.POLAR_ACCESS_TOKEN!,
server: process.env.NODE_ENV === 'production' ? 'production' : 'sandbox',
});
Variables d’environnement nécessaires :
POLAR_ACCESS_TOKEN=your_access_token
POLAR_WEBHOOK_SECRET=your_webhook_secret
Obtenez-les depuis votre dashboard Polar. Utilisez le mode sandbox pendant le développement — cela fournit des cartes de test et ne débitera pas d’argent réel.
Création de sessions de paiement #creating-checkout-sessions
Lorsqu’un utilisateur clique sur “S’abonner” ou “Acheter”, créez une session de paiement et redirigez-le :
import { polar } from '../lib/polar';
export async function createCheckout(user: { id: string; email: string }, productId: string) {
const checkout = await polar.checkouts.create({
products: [productId],
successUrl: 'https://yourapp.com/success?session_id={CHECKOUT_SESSION_ID}',
customerEmail: user.email,
metadata: {
userId: String(user.id),
},
});
return checkout.url;
}
Points clés :
metadata.userIdlie le client Polar à votre utilisateur. Vous en aurez besoin dans les webhooks.successUrlpeut inclure{CHECKOUT_SESSION_ID}que Polar remplace par l’ID réel.productsprend un tableau — vous pouvez regrouper plusieurs produits en un seul paiement.
Exemple de route Express :
router.post('/create-checkout', async (req, res) => {
const user = req.user; // de votre middleware d'auth
const { productId } = req.body;
if (!productId) {
return res.status(400).json({ error: 'Product ID required' });
}
try {
const checkoutUrl = await createCheckout(user, productId);
res.json({ url: checkoutUrl });
} catch (error) {
console.error('Checkout creation failed:', error);
res.status(500).json({ error: 'Failed to create checkout' });
}
});
Gestion des webhooks #webhook-handling
Les webhooks sont la façon dont Polar informe votre application des événements de paiement. C’est la partie la plus critique à bien configurer.
Vérification des signatures
Polar utilise la spécification Standard Webhooks. Vous devez vérifier les signatures pour empêcher les requêtes usurpées :
import { Webhook } from 'standardwebhooks';
const secret = 'whsec_' + Buffer.from(
process.env.POLAR_WEBHOOK_SECRET!
).toString('base64');
const wh = new Webhook(secret);
export function verifyWebhook(rawBody: string, headers: Record<string, string>) {
try {
return wh.verify(rawBody, headers);
} catch (error) {
console.error('Webhook verification failed:', error);
throw new Error('Invalid webhook signature');
}
}
Configuration de la route (Critique !)
La route de webhook a besoin du corps brut de la requête pour la vérification de signature. Enregistrez-la AVANT votre middleware JSON :
import express from 'express';
import { webhookRouter } from './routes/webhooks';
const app = express();
// Routes de webhook en PREMIER - besoin du corps brut
app.use('/api/webhooks', webhookRouter);
// Parsing JSON APRÈS les webhooks
app.use(express.json());
// Autres routes...
app.use('/api', apiRouter);
import { Router } from 'express';
import express from 'express';
import { verifyWebhook } from '../webhooks/polar';
import { handlePolarEvent } from '../services/polar-events';
const router = Router();
router.post('/polar',
express.raw({ type: 'application/json' }),
async (req, res) => {
try {
const event = verifyWebhook(
req.body.toString(),
req.headers as Record<string, string>
);
await handlePolarEvent(event);
res.status(200).json({ received: true });
} catch (error) {
console.error('Webhook error:', error);
res.status(400).json({ error: 'Webhook processing failed' });
}
}
);
export { router as webhookRouter };
Gestionnaires d’événements
Voici comment gérer les événements d’abonnement clés :
import { db } from '../lib/db';
interface PolarEvent {
type: string;
data: {
id: string;
status: string;
customer_id: string;
metadata?: { userId?: string };
current_period_end?: string;
};
}
export async function handlePolarEvent(event: PolarEvent) {
const { type, data } = event;
switch (type) {
case 'subscription.created':
// Lier le client Polar à votre utilisateur
await db.user.update({
where: { id: data.metadata?.userId },
data: {
polarCustomerId: data.customer_id,
subscriptionId: data.id,
subscriptionStatus: 'pending',
},
});
break;
case 'subscription.active':
// Paiement réussi - activez l'abonnement
await db.user.update({
where: { polarCustomerId: data.customer_id },
data: {
subscriptionStatus: 'active',
currentPeriodEnd: new Date(data.current_period_end!),
// Définir des limites spécifiques au plan
apiCallsLimit: 10000,
storageLimit: 5 * 1024 * 1024 * 1024, // 5GB
},
});
break;
case 'subscription.updated':
// Changement de plan, renouvellement, etc.
await db.user.update({
where: { polarCustomerId: data.customer_id },
data: {
subscriptionStatus: data.status,
currentPeriodEnd: new Date(data.current_period_end!),
},
});
break;
case 'subscription.canceled':
// Utilisateur annulé - toujours actif jusqu'à la fin de la période
await db.user.update({
where: { polarCustomerId: data.customer_id },
data: {
subscriptionStatus: 'canceled',
canceledAt: new Date(),
},
});
break;
case 'subscription.revoked':
// Révocation immédiate (paiement échoué, remboursement, etc.)
await db.user.update({
where: { polarCustomerId: data.customer_id },
data: {
subscriptionStatus: 'revoked',
apiCallsLimit: 0,
storageLimit: 0,
},
});
break;
case 'order.paid':
// Achat unique terminé
await handleOneTimePurchase(data);
break;
default:
console.log(`Unhandled event type: ${type}`);
}
}
async function handleOneTimePurchase(data: PolarEvent['data']) {
// Accorder un accès à vie, des crédits, etc.
await db.user.update({
where: { id: data.metadata?.userId },
data: {
lifetimeAccess: true,
},
});
}
Gestion des abonnements #subscription-management
Annuler un abonnement
Permettez aux utilisateurs d’annuler depuis votre application (ils conservent l’accès jusqu’à la fin de la période) :
import { polar } from '../lib/polar';
export async function cancelSubscription(subscriptionId: string) {
await polar.subscriptions.update({
id: subscriptionId,
subscriptionUpdate: {
cancelAtPeriodEnd: true,
},
});
}
Portail client
Polar fournit un portail hébergé où les clients peuvent gérer la facturation, mettre à jour les méthodes de paiement et afficher les factures :
export async function getCustomerPortalUrl(polarCustomerId: string) {
const session = await polar.customerSessions.create({
customerId: polarCustomerId,
});
return session.customerPortalUrl;
}
Route Express :
router.post('/billing-portal', async (req, res) => {
const user = req.user;
if (!user.polarCustomerId) {
return res.status(400).json({ error: 'No subscription found' });
}
try {
const portalUrl = await getCustomerPortalUrl(user.polarCustomerId);
res.json({ url: portalUrl });
} catch (error) {
console.error('Portal creation failed:', error);
res.status(500).json({ error: 'Failed to create portal session' });
}
});
Pièges courants #common-pitfalls
Après avoir intégré Polar en production, voici les erreurs que j’ai vues (et commises) :
1. Middleware JSON avant les webhooks
Si vous voyez des erreurs de vérification de signature, c’est presque toujours la cause. Le corps brut est analysé par express.json() avant que votre gestionnaire de webhook ne le voit.
Solution : Enregistrez les routes de webhook avant express.json().
2. Ne pas gérer tous les états d’abonnement
Un abonnement peut être : active, canceled, past_due, incomplete, trialing, ou revoked. Ne vérifiez pas seulement active — gérez le cycle de vie complet.
3. Métadonnées manquantes
Si vous oubliez de passer userId dans les métadonnées de paiement, vous ne pouvez pas lier le client Polar à votre utilisateur. Incluez toujours des informations d’identification.
4. Gestionnaires de webhook non idempotents
Polar peut réessayer les webhooks. Vos gestionnaires doivent être idempotents — traiter le même événement deux fois ne doit rien casser.
// Mauvais - crée des enregistrements en double
await db.payment.create({ data: { orderId: event.data.id, ... } });
// Bon - upsert basé sur un ID unique
await db.payment.upsert({
where: { polarOrderId: event.data.id },
create: { polarOrderId: event.data.id, ... },
update: { ... },
});
5. Blocage des réponses de webhook
Les endpoints de webhook doivent répondre rapidement. Si vous devez effectuer un traitement lourd, accusez réception immédiatement et traitez de manière asynchrone :
router.post('/polar', express.raw({ type: 'application/json' }), async (req, res) => {
const event = verifyWebhook(req.body.toString(), req.headers);
// Répondre immédiatement
res.status(200).json({ received: true });
// Traiter de manière asynchrone (utilisez une file d'attente de tâches appropriée en production)
setImmediate(() => handlePolarEvent(event));
});
Tester votre intégration #testing-your-integration
Utilisez le mode Sandbox
L’environnement sandbox de Polar vous permet de tester le flux complet sans frais réels :
const polar = new Polar({
accessToken: process.env.POLAR_ACCESS_TOKEN,
server: 'sandbox', // Mode test
});
Cartes de test
En sandbox, utilisez ces numéros de carte de test :
4242 4242 4242 4242- Paiement réussi4000 0000 0000 0002- Carte refusée
Test de webhook local
Utilisez ngrok ou similaire pour exposer votre serveur local :
ngrok http 3000
Puis définissez votre URL de webhook dans le dashboard Polar vers : https://your-ngrok-url.ngrok.io/api/webhooks/polar
Vérifiez votre configuration
Checklist avant la mise en production :
- Le paiement se crée avec succès et redirige vers Polar
- L’URL de succès redirige vers votre application
- La vérification de signature de webhook passe
- L’événement
subscription.activeactive l’accès utilisateur - Le portail client se charge correctement
- L’annulation fonctionne et définit
cancelAtPeriodEnd
Voilà l’intégration complète des paiements Polar.sh. L’idée clé : laissez Polar gérer ce qu’il fait de mieux (UI de paiement, traitement des paiements, portail client) et concentrez votre code sur la gestion des webhooks et les droits d’accès des utilisateurs.
Pour plus de détails, consultez la documentation officielle Polar.