Blog Sumber Daya Tentang Cari Topik
Tutorials

Polar.sh Payments di Node.js: Panduan Backend Lengkap

Diperbarui pada 3 Januari 2026

Kategori: Tutorials
Bagikan

Integrasi pembayaran Polar.sh dengan backend Node.js

Postingan ini memperluas utas X saya tentang pembayaran Polar.sh dengan contoh kode lengkap, penanganan error, dan pelajaran yang dipetik dari penggunaan produksi.

Jika Anda membangun SaaS atau menjual produk digital, Anda memerlukan penyedia pembayaran. Polar.sh telah menjadi pilihan utama saya untuk proyek indie—berikut cara mengintegrasikannya dengan backend Node.js.

Mengapa Polar.sh? #why-polar-sh

Sebelum masuk ke kode, berikut alasan saya memilih Polar.sh dibandingkan alternatif lain:

vs Stripe: Polar menangani lebih banyak secara out-of-the-box. Anda mendapatkan halaman checkout yang dihosting, portal pelanggan, dan manajemen siklus hidup langganan tanpa harus membangunnya sendiri. Stripe memberi Anda lebih banyak kendali tetapi membutuhkan lebih banyak kode.

vs Paddle/Lemon Squeezy: Manfaat merchant-of-record yang serupa (mereka menangani PPN/pajak penjualan), tetapi Polar dibangun khusus untuk pengembang dan proyek open-source. DX-nya jauh lebih baik.

Apa yang ditangani Polar untuk Anda:

  • Halaman checkout yang dihosting
  • Pemrosesan pembayaran
  • Portal pelanggan
  • Siklus hidup langganan
  • Kepatuhan pajak (sebagai merchant of record)

Apa yang Anda tangani:

  • Event webhook
  • Hak akses pengguna
  • Pelacakan penggunaan

Pengaturan Proyek #project-setup

Instal SDK dan library verifikasi webhook:

Terminal
npm install @polar-sh/sdk standardwebhooks

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

Variabel lingkungan yang dibutuhkan:

.env
POLAR_ACCESS_TOKEN=your_access_token
POLAR_WEBHOOK_SECRET=your_webhook_secret

Dapatkan ini dari dashboard Polar Anda. Gunakan mode sandbox selama pengembangan—ini menyediakan kartu tes dan tidak akan menagih uang sungguhan.

Membuat Sesi Checkout #creating-checkout-sessions

Ketika pengguna mengklik “Berlangganan” atau “Beli”, buat sesi checkout dan arahkan mereka:

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

Poin penting:

  • metadata.userId menghubungkan pelanggan Polar ke pengguna Anda. Anda akan memerlukan ini di webhook.
  • successUrl dapat menyertakan {CHECKOUT_SESSION_ID} yang akan diganti Polar dengan ID sebenarnya.
  • products menerima array—Anda dapat menggabungkan beberapa produk dalam satu checkout.

Contoh rute Express:

src/routes/api.ts
router.post('/create-checkout', async (req, res) => {
    const user = req.user; // dari middleware auth Anda
    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' });
    }
});

Penanganan Webhook #webhook-handling

Webhook adalah cara Polar memberi tahu aplikasi Anda tentang event pembayaran. Ini adalah bagian paling kritis untuk mendapatkan hasil yang benar.

Verifikasi Tanda Tangan

Polar menggunakan spesifikasi Standard Webhooks. Anda harus memverifikasi tanda tangan untuk mencegah permintaan palsu:

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

Pengaturan Rute (Kritis!)

Rute webhook memerlukan badan permintaan mentah untuk verifikasi tanda tangan. Daftarkan SEBELUM middleware JSON Anda:

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

const app = express();

// Rute webhook PERTAMA - memerlukan badan mentah
app.use('/api/webhooks', webhookRouter);

// Parsing JSON SETELAH webhook
app.use(express.json());

// Rute lainnya...
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 };

Penangan Event

Berikut cara menangani event langganan utama:

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':
            // Hubungkan pelanggan Polar ke pengguna Anda
            await db.user.update({
                where: { id: data.metadata?.userId },
                data: {
                    polarCustomerId: data.customer_id,
                    subscriptionId: data.id,
                    subscriptionStatus: 'pending',
                },
            });
            break;

        case 'subscription.active':
            // Pembayaran berhasil - aktifkan langganan
            await db.user.update({
                where: { polarCustomerId: data.customer_id },
                data: {
                    subscriptionStatus: 'active',
                    currentPeriodEnd: new Date(data.current_period_end!),
                    // Atur batas khusus paket
                    apiCallsLimit: 10000,
                    storageLimit: 5 * 1024 * 1024 * 1024, // 5GB
                },
            });
            break;

        case 'subscription.updated':
            // Perubahan paket, perpanjangan, dll.
            await db.user.update({
                where: { polarCustomerId: data.customer_id },
                data: {
                    subscriptionStatus: data.status,
                    currentPeriodEnd: new Date(data.current_period_end!),
                },
            });
            break;

        case 'subscription.canceled':
            // Pengguna membatalkan - tetap aktif hingga akhir periode
            await db.user.update({
                where: { polarCustomerId: data.customer_id },
                data: {
                    subscriptionStatus: 'canceled',
                    canceledAt: new Date(),
                },
            });
            break;

        case 'subscription.revoked':
            // Pencabutan segera (pembayaran gagal, pengembalian dana, dll.)
            await db.user.update({
                where: { polarCustomerId: data.customer_id },
                data: {
                    subscriptionStatus: 'revoked',
                    apiCallsLimit: 0,
                    storageLimit: 0,
                },
            });
            break;

        case 'order.paid':
            // Pembelian satu kali selesai
            await handleOneTimePurchase(data);
            break;

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

async function handleOneTimePurchase(data: PolarEvent['data']) {
    // Berikan akses seumur hidup, kredit, dll.
    await db.user.update({
        where: { id: data.metadata?.userId },
        data: {
            lifetimeAccess: true,
        },
    });
}

Manajemen Langganan #subscription-management

Batalkan Langganan

Izinkan pengguna membatalkan dari aplikasi Anda (mereka tetap mendapat akses hingga akhir periode):

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

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

Portal Pelanggan

Polar menyediakan portal terkelola di mana pelanggan dapat mengelola penagihan, memperbarui metode pembayaran, dan melihat faktur:

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

    return session.customerPortalUrl;
}

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

Kesalahan Umum #common-pitfalls

Setelah mengintegrasikan Polar di produksi, berikut adalah kesalahan yang telah saya lihat (dan buat):

1. Middleware JSON sebelum webhook

Jika Anda melihat error verifikasi tanda tangan, ini hampir selalu penyebabnya. Badan mentah diurai oleh express.json() sebelum handler webhook Anda melihatnya.

Perbaikan: Daftarkan rute webhook SEBELUM express.json().

2. Tidak menangani semua status langganan

Langganan bisa berupa: active, canceled, past_due, incomplete, trialing, atau revoked. Jangan hanya memeriksa active—tangani seluruh siklus hidup.

3. Metadata hilang

Jika Anda lupa mengoper userId di metadata checkout, Anda tidak dapat menghubungkan pelanggan Polar ke pengguna Anda. Selalu sertakan informasi identifikasi.

4. Handler webhook tidak idempoten

Polar mungkin mencoba ulang webhook. Handler Anda harus idempoten—memproses event yang sama dua kali tidak boleh merusak apa pun.

// Buruk - membuat catatan duplikat
await db.payment.create({ data: { orderId: event.data.id, ... } });

// Baik - upsert berdasarkan ID unik
await db.payment.upsert({
    where: { polarOrderId: event.data.id },
    create: { polarOrderId: event.data.id, ... },
    update: { ... },
});

5. Memblokir respons webhook

Endpoint webhook harus merespons dengan cepat. Jika Anda perlu melakukan pemrosesan berat, akui segera dan proses secara async:

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

    // Respon segera
    res.status(200).json({ received: true });

    // Proses async (gunakan antrian pekerjaan yang tepat di produksi)
    setImmediate(() => handlePolarEvent(event));
});

Menguji Integrasi Anda #testing-your-integration

Gunakan Mode Sandbox

Lingkungan sandbox Polar memungkinkan Anda menguji alur lengkap tanpa tagihan sungguhan:

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

Kartu Tes

Di sandbox, gunakan nomor kartu tes ini:

  • 4242 4242 4242 4242 - Pembayaran sukses
  • 4000 0000 0000 0002 - Kartu ditolak

Pengujian Webhook Lokal

Gunakan ngrok atau serupa untuk mengekspos server lokal Anda:

ngrok http 3000

Kemudian atur URL webhook Anda di dashboard Polar ke: https://your-ngrok-url.ngrok.io/api/webhooks/polar

Verifikasi Pengaturan Anda

Daftar periksa sebelum live:

  • Checkout berhasil dibuat dan diarahkan ke Polar
  • URL sukses mengarah kembali ke aplikasi Anda
  • Verifikasi tanda tangan webhook berlalu
  • Event subscription.active mengaktifkan akses pengguna
  • Portal pelanggan dimuat dengan benar
  • Pembatalan berfungsi dan menyetel cancelAtPeriodEnd

Itulah integrasi lengkap pembayaran Polar.sh. Inti kuncinya: biarkan Polar menangani apa yang baik (UI checkout, pemrosesan pembayaran, portal pelanggan) dan fokuskan kode Anda pada penanganan webhook dan hak akses pengguna.

Untuk detail lebih lanjut, periksa dokumentasi resmi Polar.

Kategori Tutorials
Bagikan

Dapatkan wawasan AI terbaru langsung di kotak masuk Anda

Tetap terkini dengan tren terbaru, tutorial, dan wawasan industri. Bergabunglah dengan komunitas pengembang yang mempercayai newsletter kami.

Hanya akun baru. Dengan mengirimkan email Anda, Anda menyetujui Kebijakan Privasi