Blog Ressources À propos Rechercher Sujets
Tutorials

Polar.sh Payments in Node.js: Guide Complète Backend

Mis à jour le 3 janvier 2026

Catégorie: Tutorials
Partager

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 :

Terminal
npm install @polar-sh/sdk standardwebhooks

Initialisez le client Polar :

src/lib/polar.ts
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 :

.env
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 :

src/routes/checkout.ts
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.userId lie le client Polar à votre utilisateur. Vous en aurez besoin dans les webhooks.
  • successUrl peut inclure {CHECKOUT_SESSION_ID} que Polar remplace par l’ID réel.
  • products prend un tableau — vous pouvez regrouper plusieurs produits en un seul paiement.

Exemple de route Express :

src/routes/api.ts
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 :

src/webhooks/polar.ts
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 :

src/app.ts
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);
src/routes/webhooks.ts
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 :

src/services/polar-events.ts
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) :

src/services/subscriptions.ts
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 :

src/services/subscriptions.ts
export async function getCustomerPortalUrl(polarCustomerId: string) {
    const session = await polar.customerSessions.create({
        customerId: polarCustomerId,
    });

    return session.customerPortalUrl;
}

Route Express :

src/routes/api.ts
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éussi
  • 4000 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.active active 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.

Catégorie Tutorials
Partager

Obtenez les derniers insights IA livrés dans votre boîte de réception

Restez à jour avec les dernières tendances, tutoriels et perspectives de l'industrie. Rejoignez la communauté des développeurs qui font confiance à notre newsletter.

Nouveaux comptes uniquement. En envoyant votre e-mail, vous acceptez nos Politique de Confidentialité