1. Pengenalan Stripe
Stripe adalah platform pemrosesan pembayaran online yang memungkinkan bisnis menerima pembayaran melalui internet. Didirikan pada tahun 2010 oleh Patrick dan John Collison, Stripe telah menjadi salah satu payment gateway paling populer di dunia, digunakan oleh jutaan bisnis dari startup hingga enterprise besar seperti Amazon, Google, dan Shopify.
Stripe menyediakan API yang elegan dan mudah digunakan, dengan dukungan untuk lebih dari 135 mata uang, berbagai metode pembayaran (kartu kredit/debit, bank transfer, e-wallet), serta fitur lengkap untuk subscription billing, marketplace, dan invoicing.
Mengapa Memilih Stripe?
| Keunggulan | Penjelasan |
|---|---|
| API Developer-Friendly | REST API yang elegan, dokumentasi luar biasa, SDK untuk berbagai bahasa |
| Checkout Pre-built | UI checkout siap pakai yang customizable dan responsif |
| 135+ Mata Uang | Dukungan mata uang global untuk bisnis internasional |
| PCI Compliant | Stripe menangani compliance PCI DSS sehingga Anda tidak perlu |
| Webhooks | Event-driven notifications untuk memproses pembayaran secara asinkron |
| Subscription Billing | Fitur lengkap untuk recurring payments, trial periods, dan invoicing |
| Stripe CLI | Tool command-line untuk testing, forwarding webhooks, dan debugging |
| Fraud Detection | Stripe Radar menggunakan machine learning untuk mendeteksi fraud |
Arsitektur Integrasi Stripe
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β CLIENT (Browser) β
β β
β ββββββββββββββββ βββββββββββββββββββββββββββββ β
β β React/ β β Stripe Elements / β β
β β Next.js β β Stripe Checkout β β
β β Frontend β β (iframe terisolasi) β β
β ββββββββ¬ββββββββ βββββββββββββββ¬ββββββββββββββ β
β β β β
β β Payment Data β Token/PM ID β
β ββββββββββββββββββββ¬ββββββββββββ β
ββββββββββββββββββββββββββββββΌββββββββββββββββββββββββββββββ
β
ββββββββββββββββββββββββββββββΌββββββββββββββββββββββββββββββ
β YOUR SERVER (Node.js) β
β ββββββββΌβββββββ β
β β API β β
β β Routes β β
β ββββββββ¬βββββββ β
β β β
β βββββββββββββββββΌββββββββββββββββ β
β ββββββββΌβββββββ ββββββββΌβββββββ βββββββΌββββββββ β
β β Create β β Handle β β Manage β β
β β Payment β β Webhooks β β Subscriptionsβ β
β ββββββββ¬βββββββ ββββββββ¬βββββββ βββββββ¬ββββββββ β
ββββββββββββββΌββββββββββββββββΌββββββββββββββββΌββββββββββββββ
β β β
ββββββββββββββΌββββββββββββββββΌββββββββββββββββΌββββββββββββββ
β βΌ βΌ βΌ β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β STRIPE API β β
β β β’ Payment Intents β’ Checkout Sessions β β
β β β’ Customers β’ Subscriptions β β
β β β’ Products/Prices β’ Webhooks Events β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β STRIPE β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
2. Setup & Instalasi
Mari kita mulai dengan menyiapkan lingkungan pengembangan untuk integrasi Stripe. Kita akan menggunakan Node.js + Express di backend dan React di frontend.
Instalasi Dependencies
# Backend: Node.js + Express npm init -y npm install express stripe cors dotenv # Frontend: React + Stripe.js npm install @stripe/stripe-js @stripe/react-stripe-js # Stripe CLI untuk testing # macOS brew install stripe/stripe-cli/stripe-cli # Windows (Scoop) scoop install stripe # Atau download dari https://stripe.com/docs/stripe-cli # Login ke Stripe stripe login
Environment Variables
# Stripe API Keys (dapatkan dari dashboard.stripe.com) STRIPE_SECRET_KEY=sk_test_xxxxxxxxxxxxxxxxxxxxxxxx STRIPE_PUBLISHABLE_KEY=pk_test_xxxxxxxxxxxxxxxxxxxxxxxx STRIPE_WEBHOOK_SECRET=whsec_xxxxxxxxxxxxxxxxxxxxxxxx # Untuk production, gunakan live keys: # STRIPE_SECRET_KEY=sk_live_xxxxxxxxxxxxxxxxxxxxxxxx # STRIPE_PUBLISHABLE_KEY=pk_live_xxxxxxxxxxxxxxxxxxxxxxxx # Server config PORT=3001 FRONTEND_URL=http://localhost:5173
SELALU simpan Secret Key di server-side dan JANGAN PERNAH mengeksposnya ke frontend. Hanya Publishable Key yang boleh digunakan di browser. Gunakan environment variables dan JANGAN commit file .env ke repository.
Stripe Client & Server Initialization
require('dotenv').config();
const express = require('express');
const cors = require('cors');
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
const app = express();
// Middleware
app.use(cors({ origin: process.env.FRONTEND_URL }));
// Webhook endpoint perlu raw body, jadi pasang SEBELUM express.json()
// app.use(express.json());
const PORT = process.env.PORT || 3001;
app.listen(PORT, () => {
console.log(`Stripe server berjalan di http://localhost:${PORT}`);
});
3. Checkout Sessions
Stripe Checkout adalah solusi pre-built yang menyediakan halaman checkout siap pakai yang aman, responsif, dan dapat dikustomisasi. Ini adalah cara tercepat dan paling aman untuk menerima pembayaran.
Membuat Checkout Session (Backend)
const express = require('express');
const router = express.Router();
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
// Endpoint untuk membuat Checkout Session
router.post('/create-checkout-session', async (req, res) => {
try {
const { items, customerId } = req.body;
// Format line items untuk Stripe
const lineItems = items.map((item) => ({
price_data: {
currency: 'idr',
product_data: {
name: item.nama,
description: item.deskripsi,
images: [item.gambar],
},
// Harga dalam satuan terkecil (IDR tanpa desimal)
unit_amount: item.harga, // contoh: 150000 = Rp 150.000
},
quantity: item.jumlah,
}));
const session = await stripe.checkout.sessions.create({
payment_method_types: ['card'],
line_items: lineItems,
mode: 'payment', // 'payment', 'subscription', atau 'setup'
customer: customerId || undefined,
customer_email: !customerId ? req.body.email : undefined,
// Redirect URLs setelah pembayaran
success_url: `${process.env.FRONTEND_URL}/checkout/success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${process.env.FRONTEND_URL}/checkout/cancel`,
// Metadata untuk tracking
metadata: {
orderId: req.body.orderId || 'ORD-' + Date.now(),
userId: req.body.userId || '',
},
// Opsi tambahan
billing_address_collection: 'required',
shipping_address_collection: {
allowed_countries: ['ID', 'SG', 'MY', 'TH', 'PH', 'VN'],
},
phone_number_collection: {
enabled: true,
},
// Diskon dan kupon
// discounts: [{ coupon: 'DISKON20' }],
// Tanggal kedaluwarsa session (30 menit)
expires_at: Math.floor(Date.now() / 1000) + 1800,
});
res.json({
sessionId: session.id,
url: session.url, // URL halaman checkout Stripe
});
} catch (error) {
console.error('Error membuat checkout session:', error.message);
res.status(500).json({ error: error.message });
}
});
// Verifikasi session setelah pembayaran berhasil
router.get('/verify-session/:sessionId', async (req, res) => {
try {
const session = await stripe.checkout.sessions.retrieve(
req.params.sessionId,
{
expand: ['line_items', 'customer', 'payment_intent'],
}
);
res.json({
status: session.payment_status,
customerEmail: session.customer_details?.email,
amountTotal: session.amount_total,
currency: session.currency,
metadata: session.metadata,
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
module.exports = router;
Frontend: Redirect ke Checkout
import { useState } from 'react';
function CheckoutButton({ items, userEmail }) {
const [loading, setLoading] = useState(false);
const handleCheckout = async () => {
setLoading(true);
try {
// Kirim request ke backend untuk membuat session
const response = await fetch(
'http://localhost:3001/api/create-checkout-session',
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: items.map((item) => ({
nama: item.nama,
deskripsi: item.deskripsi,
gambar: item.gambar,
harga: item.harga,
jumlah: item.jumlah,
})),
email: userEmail,
orderId: 'ORD-' + Date.now(),
}),
}
);
const { url } = await response.json();
// Redirect ke Stripe Checkout
if (url) {
window.location.href = url;
}
} catch (error) {
console.error('Error:', error);
alert('Gagal membuat checkout. Coba lagi.');
} finally {
setLoading(false);
}
};
return (
<button
onClick={handleCheckout}
disabled={loading || items.length === 0}
className="btn-checkout"
>
{loading ? 'β³ Memproses...' : 'π Checkout dengan Stripe'}
</button>
);
}
export default CheckoutButton;
Halaman Sukses & Verifikasi
import { useEffect, useState } from 'react';
import { useSearchParams } from 'react-router-dom';
function CheckoutSuccess() {
const [searchParams] = useSearchParams();
const [orderData, setOrderData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const sessionId = searchParams.get('session_id');
if (sessionId) {
verifyPayment(sessionId);
}
}, [searchParams]);
const verifyPayment = async (sessionId) => {
try {
const response = await fetch(
`http://localhost:3001/api/verify-session/${sessionId}`
);
const data = await response.json();
setOrderData(data);
} catch (error) {
console.error('Error verifikasi:', error);
} finally {
setLoading(false);
}
};
if (loading) return <div className="loading">Memverifikasi pembayaran...</div>;
return (
<div className="checkout-success">
<div className="success-icon">β
</div>
<h1>Pembayaran Berhasil!</h1>
{orderData && (
<div className="order-details">
<p>π§ Email: {orderData.customerEmail}</p>
<p>π° Total: Rp {orderData.amountTotal?.toLocaleString('id-ID')}</p>
<p>π Order ID: {orderData.metadata?.orderId}</p>
</div>
)}
<a href="/" className="btn-primary">Kembali ke Beranda</a>
</div>
);
}
export default CheckoutSuccess;
4. Payment Intents API
Payment Intents API memberikan kontrol lebih besar atas alur pembayaran dibanding Checkout Sessions. Ini cocok ketika Anda ingin mengkustomisasi tampilan checkout sepenuhnya menggunakan Stripe Elements.
Backend: Membuat Payment Intent
const express = require('express');
const router = express.Router();
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
// Buat Payment Intent
router.post('/create-payment-intent', async (req, res) => {
try {
const { amount, currency, metadata } = req.body;
const paymentIntent = await stripe.paymentIntents.create({
amount: Math.round(amount), // Pastikan integer
currency: currency || 'idr',
metadata: metadata || {},
automatic_payment_methods: {
enabled: true,
},
// Contoh: potongan biaya platform
// application_fee_amount: Math.round(amount * 0.05),
// transfer_data: { destination: 'acct_vendor_xxx' },
});
res.json({
clientSecret: paymentIntent.client_secret,
paymentIntentId: paymentIntent.id,
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Ambil detail Payment Intent
router.get('/payment-intent/:id', async (req, res) => {
try {
const pi = await stripe.paymentIntents.retrieve(req.params.id, {
expand: ['charges', 'customer'],
});
res.json({
id: pi.id,
status: pi.status,
amount: pi.amount,
currency: pi.currency,
created: pi.created,
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Refund
router.post('/refund', async (req, res) => {
try {
const { paymentIntentId, amount, reason } = req.body;
const refund = await stripe.refunds.create({
payment_intent: paymentIntentId,
amount: amount || undefined, // undefined = full refund
reason: reason || 'requested_by_customer',
});
res.json({
refundId: refund.id,
status: refund.status,
amount: refund.amount,
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
module.exports = router;
Frontend: Stripe Elements
import { useState, useEffect } from 'react';
import {
Elements,
CardElement,
useStripe,
useElements,
} from '@stripe/react-stripe-js';
import { loadStripe } from '@stripe/stripe-js';
// Load Stripe.js (hanya sekali)
const stripePromise = loadStripe(
import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY
);
// Komponen form pembayaran internal
function PaymentForm({ amount, onSuccess, onError }) {
const stripe = useStripe();
const elements = useElements();
const [clientSecret, setClientSecret] = useState('');
const [processing, setProcessing] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
// Minta client secret dari server
fetch('http://localhost:3001/api/create-payment-intent', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ amount, currency: 'idr' }),
})
.then((res) => res.json())
.then((data) => setClientSecret(data.clientSecret))
.catch((err) => setError('Gagal memulai pembayaran'));
}, [amount]);
const handleSubmit = async (e) => {
e.preventDefault();
setProcessing(true);
setError(null);
if (!stripe || !elements) return;
const cardElement = elements.getElement(CardElement);
// Konfirmasi pembayaran
const { error: stripeError, paymentIntent } =
await stripe.confirmCardPayment(clientSecret, {
payment_method: {
card: cardElement,
billing_details: {
name: e.target.nama.value,
email: e.target.email.value,
},
},
});
if (stripeError) {
setError(stripeError.message);
setProcessing(false);
onError?.(stripeError);
return;
}
if (paymentIntent.status === 'succeeded') {
setProcessing(false);
onSuccess?.(paymentIntent);
}
};
const cardElementOptions = {
style: {
base: {
fontSize: '16px',
color: '#ffffff',
backgroundColor: '#1a1a2e',
'::placeholder': { color: '#888' },
},
invalid: { color: '#ff4444' },
},
hidePostalCode: true,
};
return (
<form onSubmit={handleSubmit} className="payment-form">
<div className="form-group">
<label>Nama Lengkap</label>
<input type="text" name="nama" required />
</div>
<div className="form-group">
<label>Email</label>
<input type="email" name="email" required />
</div>
<div className="form-group">
<label>Detail Kartu</label>
<div className="card-element-wrapper">
<CardElement options={cardElementOptions} />
</div>
</div>
{error && <p className="error">{error}</p>}
<button type="submit" disabled={!stripe || processing}>
{processing
? 'β³ Memproses...'
: `π³ Bayar Rp ${amount.toLocaleString('id-ID')}`}
</button>
</form>
);
}
// Wrapper komponen dengan Elements provider
function StripePaymentForm({ amount, onSuccess, onError }) {
return (
<Elements stripe={stripePromise}>
<PaymentForm amount={amount} onSuccess={onSuccess} onError={onError} />
</Elements>
);
}
export default StripePaymentForm;
5. Webhooks: Event Handling
Webhooks adalah cara Stripe memberitahu server Anda tentang peristiwa yang terjadi β pembayaran berhasil, refund, langganan diperbarui, dll. Ini sangat penting karena tidak semua operasi bisa diproses secara sinkron di frontend.
Mengapa Webhooks Penting?
- User bisa menutup browser setelah pembayaran β webhook memastikan server tetap mendapat notifikasi
- Beberapa pembayaran butuh waktu (bank transfer, async payment methods)
- Subscription events (renewal, cancellation) terjadi tanpa interaksi user
- Menjadi sumber kebenaran (source of truth) untuk status pembayaran
Webhook Events yang Umum
| Event | Keterangan | Aksi |
|---|---|---|
| checkout.session.completed | Checkout selesai | Buat order, kirim email konfirmasi |
| payment_intent.succeeded | Pembayaran berhasil | Update status order |
| payment_intent.payment_failed | Pembayaran gagal | Notifikasi ke user |
| invoice.paid | Invoice subscription dibayar | Perpanjang langganan |
| invoice.payment_failed | Pembayaran invoice gagal | Notifikasi, grace period |
| customer.subscription.created | Langganan baru dibuat | Aktifkan fitur premium |
| customer.subscription.deleted | Langganan dibatalkan | Nonaktifkan fitur premium |
| charge.refunded | Refund diproses | Update order, kirim notifikasi |
Implementasi Webhook Endpoint
const express = require('express');
const router = express.Router();
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
// PENTING: Webhook endpoint harus menerima raw body
router.post(
'/webhook',
express.raw({ type: 'application/json' }),
async (req, res) => {
const sig = req.headers['stripe-signature'];
let event;
// Verifikasi signature webhook
try {
event = stripe.webhooks.constructEvent(
req.body,
sig,
process.env.STRIPE_WEBHOOK_SECRET
);
} catch (err) {
console.error('β οΈ Webhook signature verification gagal:', err.message);
return res.status(400).send(`Webhook Error: ${err.message}`);
}
console.log(`π¨ Webhook diterima: ${event.type} [${event.id}]`);
// Handle berbagai event types
switch (event.type) {
// ============ CHECKOUT EVENTS ============
case 'checkout.session.completed': {
const session = event.data.object;
console.log('β
Checkout selesai:', session.id);
// Aksi:
// 1. Buat order di database
// 2. Kirim email konfirmasi
// 3. Update inventory
await buatOrder({
sessionId: session.id,
customerId: session.customer,
email: session.customer_details.email,
amountTotal: session.amount_total,
metadata: session.metadata,
});
break;
}
// ============ PAYMENT EVENTS ============
case 'payment_intent.succeeded': {
const paymentIntent = event.data.object;
console.log('π° Pembayaran berhasil:', paymentIntent.id);
// Update status order di database
await updateOrderStatus(paymentIntent.metadata.orderId, 'paid');
break;
}
case 'payment_intent.payment_failed': {
const failedPI = event.data.object;
console.log('β Pembayaran gagal:', failedPI.id);
// Notifikasi ke user, update status
await updateOrderStatus(failedPI.metadata.orderId, 'payment_failed');
await kirimNotifikasi(
failedPI.metadata.userId,
'Pembayaran Anda gagal. Silakan coba lagi.'
);
break;
}
// ============ SUBSCRIPTION EVENTS ============
case 'customer.subscription.created': {
const subscription = event.data.object;
console.log('π Langganan baru:', subscription.id);
// Aktifkan fitur premium
await activateSubscription(
subscription.metadata.userId,
subscription.id,
subscription.items.data[0].price.id
);
break;
}
case 'customer.subscription.updated': {
const subscription = event.data.object;
console.log('π Langganan diperbarui:', subscription.id);
// Update plan jika berubah
await updateSubscription(subscription.id, {
status: subscription.status,
priceId: subscription.items.data[0].price.id,
currentPeriodEnd: subscription.current_period_end,
});
break;
}
case 'customer.subscription.deleted': {
const subscription = event.data.object;
console.log('π« Langganan dibatalkan:', subscription.id);
// Nonaktifkan fitur premium
await deactivateSubscription(subscription.id);
break;
}
// ============ INVOICE EVENTS ============
case 'invoice.paid': {
const invoice = event.data.object;
console.log('π Invoice dibayar:', invoice.id);
// Perpanjang langganan
await renewSubscription(invoice.subscription);
break;
}
case 'invoice.payment_failed': {
const invoice = event.data.object;
console.log('π Invoice gagal dibayar:', invoice.id);
// Mulai grace period, kirim email peringatan
await startGracePeriod(invoice.subscription);
await kirimEmailPeringatan(invoice.customer, 'invoice_failed');
break;
}
default:
console.log(`βΉοΈ Event tidak ditangani: ${event.type}`);
}
// Selalu kembalikan 200 agar Stripe tidak retry
res.json({ received: true });
}
);
// Contoh helper functions (implementasikan sesuai database Anda)
async function buatOrder(data) {
console.log('Membuat order:', data.metadata.orderId);
// Implementasi: simpan ke database
}
async function updateOrderStatus(orderId, status) {
console.log(`Update order ${orderId}: ${status}`);
// Implementasi: update database
}
async function activateSubscription(userId, subscriptionId, priceId) {
console.log(`Aktifkan langganan user ${userId}`);
// Implementasi: update database user
}
async function updateSubscription(subscriptionId, data) {
console.log(`Update langganan ${subscriptionId}:`, data);
}
async function deactivateSubscription(subscriptionId) {
console.log(`Nonaktifkan langganan ${subscriptionId}`);
}
async function renewSubscription(subscriptionId) {
console.log(`Perpanjang langganan ${subscriptionId}`);
}
async function startGracePeriod(subscriptionId) {
console.log(`Mulai grace period untuk ${subscriptionId}`);
}
async function kirimNotifikasi(userId, message) {
console.log(`Notifikasi ke ${userId}: ${message}`);
}
async function kirimEmailPeringatan(customerId, type) {
console.log(`Email peringatan ke ${customerId}: ${type}`);
}
module.exports = router;
Gunakan Stripe CLI untuk menerima webhook di localhost:
stripe listen --forward-to localhost:3001/api/webhook
CLI akan memberikan webhook signing secret yang bisa Anda masukkan ke .env sebagai STRIPE_WEBHOOK_SECRET.
6. Subscription Billing
Stripe memungkinkan Anda membuat recurring payments (pembayaran berulang) untuk model bisnis berbasis langganan. Anda bisa membuat produk dengan harga bulanan/tahunan, menawarkan trial period, dan mengelola siklus tagihan secara otomatis.
Membuat Produk dan Harga
const express = require('express');
const router = express.Router();
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
// Buat Checkout Session untuk Subscription
router.post('/create-subscription', async (req, res) => {
try {
const { email, priceId, trialDays } = req.body;
// 1. Buat atau cari customer
let customer;
const existingCustomers = await stripe.customers.list({
email: email,
limit: 1,
});
if (existingCustomers.data.length > 0) {
customer = existingCustomers.data[0];
} else {
customer = await stripe.customers.create({
email: email,
metadata: { source: 'beebane-app' },
});
}
// 2. Buat Checkout Session untuk subscription
const session = await stripe.checkout.sessions.create({
customer: customer.id,
payment_method_types: ['card'],
line_items: [
{
price: priceId, // Harga yang dibuat di Stripe Dashboard
quantity: 1,
},
],
mode: 'subscription',
success_url: `${process.env.FRONTEND_URL}/subscription/success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${process.env.FRONTEND_URL}/subscription/cancel`,
// Trial period (opsional)
subscription_data: {
trial_period_days: trialDays || 7,
metadata: {
userId: req.body.userId || '',
plan: req.body.planName || '',
},
},
// Koleksi metode pembayaran untuk retry
payment_method_collection: 'always',
});
res.json({
sessionId: session.id,
url: session.url,
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Ubah langganan (upgrade/downgrade)
router.post('/change-subscription', async (req, res) => {
try {
const { subscriptionId, newPriceId } = req.body;
const subscription = await stripe.subscriptions.retrieve(subscriptionId);
const updatedSubscription = await stripe.subscriptions.update(
subscriptionId,
{
items: [
{
id: subscription.items.data[0].id,
price: newPriceId,
},
],
proration_behavior: 'create_prorations', // Hitung selisih harga
}
);
res.json({
subscriptionId: updatedSubscription.id,
status: updatedSubscription.status,
currentPeriodEnd: updatedSubscription.current_period_end,
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Batalkan langganan
router.post('/cancel-subscription', async (req, res) => {
try {
const { subscriptionId, cancelImmediately } = req.body;
let subscription;
if (cancelImmediately) {
// Batalkan langsung
subscription = await stripe.subscriptions.cancel(subscriptionId);
} else {
// Batalkan di akhir periode (tetap aktif sampai habis)
subscription = await stripe.subscriptions.update(subscriptionId, {
cancel_at_period_end: true,
});
}
res.json({
subscriptionId: subscription.id,
status: subscription.status,
cancelAt: subscription.cancel_at,
canceledAt: subscription.canceled_at,
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Dapatkan detail langganan user
router.get('/subscription/:customerId', async (req, res) => {
try {
const subscriptions = await stripe.subscriptions.list({
customer: req.params.customerId,
status: 'active',
expand: ['data.default_payment_method', 'data.items.data.price'],
});
if (subscriptions.data.length === 0) {
return res.json({ subscription: null });
}
const sub = subscriptions.data[0];
res.json({
subscription: {
id: sub.id,
status: sub.status,
plan: sub.items.data[0].price.nickname,
amount: sub.items.data[0].price.unit_amount,
currency: sub.items.data[0].price.currency,
interval: sub.items.data[0].price.recurring.interval,
currentPeriodStart: sub.current_period_start,
currentPeriodEnd: sub.current_period_end,
cancelAtPeriodEnd: sub.cancel_at_period_end,
trialEnd: sub.trial_end,
},
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
module.exports = router;
7. Customer Portal
Stripe Customer Portal adalah halaman self-service yang bisa Anda berikan kepada pelanggan untuk mengelola langganan mereka β mengganti metode pembayaran, melihat riwayat invoice, mengunduh faktur, dan membatalkan langganan.
Membuat Customer Portal Session
const express = require('express');
const router = express.Router();
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
// Buat Customer Portal Session
router.post('/create-portal-session', async (req, res) => {
try {
const { customerId } = req.body;
const session = await stripe.billingPortal.sessions.create({
customer: customerId,
return_url: `${process.env.FRONTEND_URL}/dashboard`,
});
res.json({ url: session.url });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
module.exports = router;
Customer Portal perlu dikonfigurasi di Dashboard Stripe β Settings β Customer Portal. Di sana Anda bisa menentukan:
- Apakah pelanggan boleh mengganti plan
- Apakah pelanggan boleh membatalkan langganan
- Metode pembayaran yang diperbolehkan
- Informasi bisnis yang ditampilkan
8. Keamanan & Best Practices
Keamanan adalah aspek krusial dalam integrasi pembayaran. Berikut adalah best practices yang harus diikuti:
Checklist Keamanan
| Aspek | Best Practice | Prioritas |
|---|---|---|
| API Keys | Simpan Secret Key di environment variables, JANGAN di client-side | π΄ Kritis |
| Webhook Signature | Selalu verifikasi signature webhook dengan constructEvent() | π΄ Kritis |
| HTTPS | Gunakan HTTPS di production untuk semua endpoint | π΄ Kritis |
| Idempotency | Gunakan idempotency key untuk prevent duplicate charges | π‘ Penting |
| Input Validation | Validasi amount dan currency di server-side | π‘ Penting |
| PCI Compliance | Gunakan Stripe Elements / Checkout (data kartu tidak melewati server Anda) | π΄ Kritis |
| Error Handling | Jangan expose error Stripe ke user secara langsung | π‘ Penting |
| Logging | Log semua payment events tapi JANGAN log kartu/kredensial | π’ Baik |
Idempotent Requests
// Mencegah double charge jika user klik tombol bayar berkali-kali
router.post('/create-payment', async (req, res) => {
try {
// Generate idempotency key dari request (unik per operasi)
const idempotencyKey = req.headers['idempotency-key'] || `pay-${Date.now()}`;
const paymentIntent = await stripe.paymentIntents.create(
{
amount: req.body.amount,
currency: 'idr',
metadata: { orderId: req.body.orderId },
},
{
idempotencyKey: idempotencyKey,
}
);
res.json({ clientSecret: paymentIntent.client_secret });
} catch (error) {
// Stripe akan mengembalikan response yang sama jika
// idempotency key sudah pernah dipakai
res.status(500).json({ error: error.message });
}
});
9. Testing dengan Stripe CLI
Stripe CLI adalah tool command-line yang sangat berguna untuk development dan testing. Anda bisa melakukan trigger events, forward webhooks ke localhost, dan bahkan membuka Stripe Dashboard dari terminal.
Perintah Berguna
# Login ke Stripe account stripe login # Forward webhook events ke localhost stripe listen --forward-to localhost:3001/api/webhook # Output: # > Ready! Your webhook signing secret is whsec_xxxxx # (Salin secret ini ke .env Anda) # Trigger test event stripe trigger checkout.session.completed stripe trigger payment_intent.succeeded stripe trigger payment_intent.payment_failed stripe trigger customer.subscription.created stripe trigger customer.subscription.deleted stripe trigger invoice.paid stripe trigger invoice.payment_failed # Lihat daftar event yang tersedia stripe trigger --help # Buka Stripe Dashboard di browser stripe dashboard # List semua customers stripe customers list --limit 5 # List semua payment intents stripe payment_intents list --limit 5 # List semua subscriptions stripe subscriptions list --limit 5 # Inspect event stripe events retrieve evt_xxxxx
Kartu Uji (Test Cards)
| Nomor Kartu | Hasil | Kegunaan |
|---|---|---|
| 4242 4242 4242 4242 | β Berhasil | Test pembayaran sukses |
| 4000 0025 0000 3155 | π 3D Secure | Test autentikasi 3D Secure |
| 4000 0000 0000 0002 | β Ditolak | Test kartu ditolak |
| 4000 0000 0000 9995 | β Dana kurang | Test insufficient funds |
| 4000 0000 0000 0341 | β Fraud | Test failed (attach to customer) |
- Gunakan test mode (sk_test_*) selama development
- Gunakan
stripe listen --forward-tountuk webhook lokal - Test semua skenario: sukses, gagal, 3D Secure, refund
- Simulate subscription lifecycle: create β trial β active β renew β cancel
- Switch ke live mode (sk_live_*) hanya setelah semua teruji
10. Quiz Pemahaman
Uji pemahaman Anda tentang Stripe Payment Integration:
1. Key mana yang AMAN digunakan di frontend browser?
2. Mengapa webhook signature verification penting?
3. Apa fungsi cancel_at_period_end: true?
4. Mode apa yang digunakan di Checkout Session untuk subscription?
5. Command apa untuk forward webhook ke localhost?