Blog Ressourcen Über Suche Themen
Tutorials

Polar.sh Payments in Node.js: Komplettes Backend-Handbuch

Aktualisiert am 3. Januar 2026

Kategorie: Tutorials
Teilen

Polar.sh-Zahlungsintegration mit Node.js-Backend

Dieser Beitrag erweitert meinen X-Thread über Polar.sh-Zahlungen mit vollständigen Code-Beispielen, Fehlerbehandlung und Lektionen aus dem Produktiveinsatz.

Wenn Sie ein SaaS oder digitale Produkte verkaufen, benötigen Sie einen Zahlungsanbieter. Polar.sh ist für mich zum Standard für Indie-Projekte geworden – hier ist, wie man es mit einem Node.js-Backend integriert.

Warum Polar.sh? #why-polar-sh

Bevor wir in den Code einsteigen, hier sind die Gründe, warum ich Polar.sh gegenüber Alternativen gewählt habe:

vs Stripe: Polar erledigt mehr von Haus aus. Sie erhalten gehostete Checkout-Seiten, ein Kundenportal und Abo-Lebenszyklus-Management, ohne diese selbst bauen zu müssen. Stripe gibt Ihnen mehr Kontrolle, erfordert aber mehr Code.

vs Paddle/Lemon Squeezy: Ähnliche Vorteile als Merchant of Record (sie kümmern sich um MwSt./Sales Tax), aber Polar wurde speziell für Entwickler und Open-Source-Projekte entwickelt. Das DX (Developer Experience) ist deutlich besser.

Was Polar für Sie erledigt:

  • Gehostete Checkout-Seiten
  • Zahlungsabwicklung
  • Kundenportal
  • Abo-Lebenszyklus
  • Steuerkonformität (als Merchant of Record)

Was Sie erledigen:

  • Webhook-Events
  • User-Berechtigungen
  • Nutzungserfassung

Projekt-Einrichtung #project-setup

Installieren Sie das SDK und die Webhook-Verifizierungsbibliothek:

Terminal
npm install @polar-sh/sdk standardwebhooks

Initialisieren Sie den Polar-Client:

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',
});

Benötigte Umgebungsvariablen:

.env
POLAR_ACCESS_TOKEN=your_access_token
POLAR_WEBHOOK_SECRET=your_webhook_secret

Holen Sie diese von Ihrem Polar-Dashboard. Verwenden Sie während der Entwicklung den Sandbox-Modus – dieser stellt Testkarten bereit und belastet kein echtes Geld.

Checkout-Sessions erstellen #creating-checkout-sessions

Wenn ein Benutzer auf “Abonnieren” oder “Kaufen” klickt, erstellen Sie eine Checkout-Session und leiten Sie ihn weiter:

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;
}

Wichtige Punkte:

  • metadata.userId verknüpft den Polar-Kunden mit Ihrem Benutzer. Dies benötigen Sie in Webhooks.
  • successUrl kann {CHECKOUT_SESSION_ID} enthalten, das Polar durch die tatsächliche ID ersetzt.
  • products nimmt ein Array – Sie können mehrere Produkte in einem Checkout bündeln.

Express-Route-Beispiel:

src/routes/api.ts
router.post('/create-checkout', async (req, res) => {
    const user = req.user; // aus Ihrer Auth-Middleware
    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' });
    }
});

Webhook-Handling #webhook-handling

Webhooks sind die Methode, mit der Polar Ihre App über Zahlungsereignisse informiert. Dies ist der kritischste Teil, den Sie richtig hinbekommen müssen.

Signatur-Verifizierung

Polar verwendet die Standard Webhooks-Spezifikation. Sie müssen Signaturen verifizieren, um gefälschte Anfragen zu verhindern:

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');
    }
}

Route-Einrichtung (Kritisch!)

Die Webhook-Route benötigt den rohen Request-Body für die Signatur-Verifizierung. Registrieren Sie sie BEVOR Ihrer JSON-Middleware:

src/app.ts
import express from 'express';
import { webhookRouter } from './routes/webhooks';

const app = express();

// Webhook-Routes ZUERST - benötigen rohen Body
app.use('/api/webhooks', webhookRouter);

// JSON-Parsing NACH Webhooks
app.use(express.json());

// Andere Routen...
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 };

Event-Handler

Hier ist, wie Sie die wichtigsten Abo-Events behandeln:

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':
            // Verknüpft Polar-Kunden mit Ihrem Benutzer
            await db.user.update({
                where: { id: data.metadata?.userId },
                data: {
                    polarCustomerId: data.customer_id,
                    subscriptionId: data.id,
                    subscriptionStatus: 'pending',
                },
            });
            break;

        case 'subscription.active':
            // Zahlung erfolgreich - Abo aktivieren
            await db.user.update({
                where: { polarCustomerId: data.customer_id },
                data: {
                    subscriptionStatus: 'active',
                    currentPeriodEnd: new Date(data.current_period_end!),
                    // Setze planspezifische Limits
                    apiCallsLimit: 10000,
                    storageLimit: 5 * 1024 * 1024 * 1024, // 5GB
                },
            });
            break;

        case 'subscription.updated':
            // Plan-Änderung, Verlängerung, 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':
            // Benutzer gekündigt - bleibt bis Periodenende aktiv
            await db.user.update({
                where: { polarCustomerId: data.customer_id },
                data: {
                    subscriptionStatus: 'canceled',
                    canceledAt: new Date(),
                },
            });
            break;

        case 'subscription.revoked':
            // Sofortige Entziehung (Zahlung fehlgeschlagen, Rückerstattung, etc.)
            await db.user.update({
                where: { polarCustomerId: data.customer_id },
                data: {
                    subscriptionStatus: 'revoked',
                    apiCallsLimit: 0,
                    storageLimit: 0,
                },
            });
            break;

        case 'order.paid':
            // Einmaliger Kauf abgeschlossen
            await handleOneTimePurchase(data);
            break;

        default:
            console.log(`Unhandled event type: ${type}`);
    }
}

async function handleOneTimePurchase(data: PolarEvent['data']) {
    // Gewähre lebenslangen Zugang, Guthaben, etc.
    await db.user.update({
        where: { id: data.metadata?.userId },
        data: {
            lifetimeAccess: true,
        },
    });
}

Abo-Verwaltung #subscription-management

Abo kündigen

Lassen Sie Benutzer aus Ihrer App kündigen (sie behalten den Zugang bis Periodenende):

src/services/subscriptions.ts
import { polar } from '../lib/polar';

export async function cancelSubscription(subscriptionId: string) {
    await polar.subscriptions.update({
        id: subscriptionId,
        subscriptionUpdate: {
            cancelAtPeriodEnd: true,
        },
    });
}

Kundenportal

Polar stellt ein gehostetes Portal bereit, in dem Kunden die Abrechnung verwalten, Zahlungsmethoden aktualisieren und Rechnungen einsehen können:

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

    return session.customerPortalUrl;
}

Express-Route:

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' });
    }
});

Häufige Fallstricke #common-pitfalls

Nach der Integration von Polar in der Produktion hier die Fehler, die ich gesehen (und gemacht) habe:

1. JSON-Middleware vor Webhooks

Wenn Sie Signatur-Verifizierungsfehler sehen, ist dies fast immer die Ursache. Der rohe Body wird von express.json() geparst, bevor Ihre Webhook-Handler ihn sehen.

Fix: Registrieren Sie Webhook-Routes vor express.json().

2. Nicht alle Abo-Zustände behandeln

Ein Abo kann sein: active, canceled, past_due, incomplete, trialing oder revoked. Prüfen Sie nicht nur auf active – behandeln Sie den gesamten Lebenszyklus.

3. Fehlende Metadaten

Wenn Sie vergessen, userId in den Checkout-Metadaten zu übergeben, können Sie den Polar-Kunden nicht mit Ihrem Benutzer verknüpfen. Fügen Sie immer identifizierende Informationen hinzu.

4. Nicht idempotente Webhook-Handler

Polar kann Webhooks wiederholen. Ihre Handler sollten idempotent sein – die Verarbeitung desselben Events zweimal sollte nichts kaputt machen.

// Schlecht - erzeugt doppelte Datensätze
await db.payment.create({ data: { orderId: event.data.id, ... } });

// Gut - Upsert basierend auf eindeutiger ID
await db.payment.upsert({
    where: { polarOrderId: event.data.id },
    create: { polarOrderId: event.data.id, ... },
    update: { ... },
});

5. Blockierende Webhook-Antworten

Webhook-Endpunkte sollten schnell antworten. Wenn Sie eine schwere Verarbeitung benötigen, bestätigen Sie sofort und verarbeiten Sie asynchron:

router.post('/polar', express.raw({ type: 'application/json' }), async (req, res) => {
    const event = verifyWebhook(req.body.toString(), req.headers);

    // Sofort antworten
    res.status(200).json({ received: true });

    // Asynchron verarbeiten (verwenden Sie in Produktion eine saubere Job-Queue)
    setImmediate(() => handlePolarEvent(event));
});

Integration testen #testing-your-integration

Sandbox-Modus verwenden

Die Sandbox-Umgebung von Polar ermöglicht das Testen des gesamten Flusses ohne echte Belastungen:

const polar = new Polar({
    accessToken: process.env.POLAR_ACCESS_TOKEN,
    server: 'sandbox', // Testmodus
});

Testkarten

Verwenden Sie in der Sandbox diese Testkartennummern:

  • 4242 4242 4242 4242 - Erfolgreiche Zahlung
  • 4000 0000 0000 0002 - Karte abgelehnt

Lokales Webhook-Testing

Verwenden Sie ngrok oder ähnlich, um Ihren lokalen Server zu exposen:

ngrok http 3000

Setzen Sie dann Ihre Webhook-URL im Polar-Dashboard auf: https://your-ngrok-url.ngrok.io/api/webhooks/polar

Setup prüfen

Checkliste vor dem Go-Live:

  • Checkout wird erfolgreich erstellt und leitet zu Polar weiter
  • Erfolgs-URL leitet zurück zu Ihrer App
  • Webhook-Signatur-Verifizierung funktioniert
  • subscription.active-Event aktiviert Benutzerzugang
  • Kundenportal lädt korrekt
  • Kündigung funktioniert und setzt cancelAtPeriodEnd

Das war die komplette Polar.sh-Zahlungsintegration. Die wichtigste Erkenntnis: Lassen Sie Polar machen, was es gut kann (Checkout-UI, Zahlungsabwicklung, Kundenportal) und konzentrieren Sie Ihren Code auf Webhook-Handling und Benutzerberechtigungen.

Weitere Details finden Sie in der offiziellen Polar-Dokumentation.

Kategorie Tutorials
Teilen

Erhalten Sie die neuesten KI-Erkenntnisse in Ihrem Posteingang

Bleiben Sie mit den neuesten Trends, Tutorials und Brancheneinblicken auf dem Laufenden. Treten Sie der Gemeinschaft von Entwicklern bei, die unserem Newsletter vertrauen.

Nur neue Konten. Durch die Übermittlung Ihrer E-Mail akzeptieren Sie unsere Datenschutzrichtlinie