Polar.sh Payments di Node.js: Panduan Backend Lengkap
Diperbarui pada 3 Januari 2026
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:
npm install @polar-sh/sdk standardwebhooks
Inisialisasi klien 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',
});
Variabel lingkungan yang dibutuhkan:
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:
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.userIdmenghubungkan pelanggan Polar ke pengguna Anda. Anda akan memerlukan ini di webhook.successUrldapat menyertakan{CHECKOUT_SESSION_ID}yang akan diganti Polar dengan ID sebenarnya.productsmenerima array—Anda dapat menggabungkan beberapa produk dalam satu checkout.
Contoh rute Express:
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:
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:
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);
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:
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):
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:
export async function getCustomerPortalUrl(polarCustomerId: string) {
const session = await polar.customerSessions.create({
customerId: polarCustomerId,
});
return session.customerPortalUrl;
}
Rute 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' });
}
});
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 sukses4000 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.activemengaktifkan 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.