Polar.sh Payments in Node.js: Komplettes Backend-Handbuch
Aktualisiert am 3. Januar 2026
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:
npm install @polar-sh/sdk standardwebhooks
Initialisieren Sie den Polar-Client:
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:
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:
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.userIdverknüpft den Polar-Kunden mit Ihrem Benutzer. Dies benötigen Sie in Webhooks.successUrlkann{CHECKOUT_SESSION_ID}enthalten, das Polar durch die tatsächliche ID ersetzt.productsnimmt ein Array – Sie können mehrere Produkte in einem Checkout bündeln.
Express-Route-Beispiel:
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:
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:
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);
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:
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):
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:
export async function getCustomerPortalUrl(polarCustomerId: string) {
const session = await polar.customerSessions.create({
customerId: polarCustomerId,
});
return session.customerPortalUrl;
}
Express-Route:
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 Zahlung4000 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.