Web Development

Stripe Payment Integration: Panduan Lengkap

Tutorial lengkap integrasi Stripe Payment β€” Checkout Sessions, Webhooks, Subscriptions, pembayaran satu kali, dan best practices keamanan dengan contoh kode Node.js dan React

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-FriendlyREST API yang elegan, dokumentasi luar biasa, SDK untuk berbagai bahasa
Checkout Pre-builtUI checkout siap pakai yang customizable dan responsif
135+ Mata UangDukungan mata uang global untuk bisnis internasional
PCI CompliantStripe menangani compliance PCI DSS sehingga Anda tidak perlu
WebhooksEvent-driven notifications untuk memproses pembayaran secara asinkron
Subscription BillingFitur lengkap untuk recurring payments, trial periods, dan invoicing
Stripe CLITool command-line untuk testing, forwarding webhooks, dan debugging
Fraud DetectionStripe Radar menggunakan machine learning untuk mendeteksi fraud

Arsitektur Integrasi Stripe

Diagram: Arsitektur Stripe Payment Flow
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                     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

Bash
# 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

.env
# 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
⚠️ Keamanan API Keys

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

JavaScript β€” server/stripe.js
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)

JavaScript β€” server/routes/checkout.js
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

JSX β€” src/components/CheckoutButton.jsx
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

JSX β€” src/pages/CheckoutSuccess.jsx
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

JavaScript β€” server/routes/payment.js
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

JSX β€” src/components/StripePaymentForm.jsx
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?

Webhook Events yang Umum

Event Keterangan Aksi
checkout.session.completedCheckout selesaiBuat order, kirim email konfirmasi
payment_intent.succeededPembayaran berhasilUpdate status order
payment_intent.payment_failedPembayaran gagalNotifikasi ke user
invoice.paidInvoice subscription dibayarPerpanjang langganan
invoice.payment_failedPembayaran invoice gagalNotifikasi, grace period
customer.subscription.createdLangganan baru dibuatAktifkan fitur premium
customer.subscription.deletedLangganan dibatalkanNonaktifkan fitur premium
charge.refundedRefund diprosesUpdate order, kirim notifikasi

Implementasi Webhook Endpoint

JavaScript β€” server/routes/webhooks.js
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;
πŸ’‘ Testing Webhooks Lokal

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

JavaScript β€” server/routes/subscriptions.js
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

JavaScript β€” server/routes/portal.js
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;
πŸ“‹ Konfigurasi Customer Portal

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 KeysSimpan Secret Key di environment variables, JANGAN di client-sideπŸ”΄ Kritis
Webhook SignatureSelalu verifikasi signature webhook dengan constructEvent()πŸ”΄ Kritis
HTTPSGunakan HTTPS di production untuk semua endpointπŸ”΄ Kritis
IdempotencyGunakan idempotency key untuk prevent duplicate charges🟑 Penting
Input ValidationValidasi amount dan currency di server-side🟑 Penting
PCI ComplianceGunakan Stripe Elements / Checkout (data kartu tidak melewati server Anda)πŸ”΄ Kritis
Error HandlingJangan expose error Stripe ke user secara langsung🟑 Penting
LoggingLog semua payment events tapi JANGAN log kartu/kredensial🟒 Baik

Idempotent Requests

JavaScript β€” Idempotency
// 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

Bash β€” Stripe CLI
# 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βœ… BerhasilTest pembayaran sukses
4000 0025 0000 3155πŸ” 3D SecureTest autentikasi 3D Secure
4000 0000 0000 0002❌ DitolakTest kartu ditolak
4000 0000 0000 9995❌ Dana kurangTest insufficient funds
4000 0000 0000 0341❌ FraudTest failed (attach to customer)
πŸ’‘ Tips Testing
  • Gunakan test mode (sk_test_*) selama development
  • Gunakan stripe listen --forward-to untuk 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?

πŸ” Zoom
100%
🎨 Tema