Step-by-step integrasi payment gateway Midtrans dan Xendit untuk marketplace produk digital. Termasuk tips handling webhook, refund, dan subscription.
Payment gateway adalah jantung dari setiap marketplace produk digital. Di Indonesia, dua pemain utama yang mendominasi adalah Midtrans (milik GoTo/Gojek) dan Xendit. Keduanya menawarkan API yang developer-friendly, tapi masing-masing punya kelebihan dan kekurangan yang perlu dipahami sebelum Anda memilih. Artikel ini memberikan panduan lengkap integrasi kedua payment gateway ini untuk marketplace produk digital.
Perbandingan Midtrans vs Xendit
Midtrans
- Kelebihan: Payment method terlengkap (GoPay, ShopeePay, QRIS, VA semua bank, kartu kredit, Alfamart/Indomaret), Snap (hosted payment page) yang sangat mudah diintegrasikan, dokumentasi dalam Bahasa Indonesia yang excellent.
- Kekurangan: Proses verifikasi merchant yang ketat (bisa 1-2 minggu), minimum payout Rp 50K, dashboard yang agak kompleks untuk pemula.
- Fee: QRIS 0.7%, VA transfer Rp 4.000, GoPay/ShopeePay 2%, Credit Card 2.9% + Rp 2.000
- Ideal untuk: Marketplace besar, e-commerce dengan banyak payment method, bisnis yang butuh GoPay/ShopeePay integration.
Xendit
- Kelebihan: Setup lebih cepat (verifikasi 1-3 hari), API yang lebih simpel dan konsisten, fitur disbursement (kirim uang ke banyak rekening) yang unggul, webhook reliability yang excellent.
- Kekurangan: Payment method sedikit lebih terbatas (tidak ada GoPay native), dokumentasi utama dalam Bahasa Inggris.
- Fee: QRIS 0.7%, VA transfer Rp 4.500, OVO 2%, Credit Card 2.9% + Rp 2.000
- Ideal untuk: SaaS, subscription billing, marketplace yang perlu disbursement otomatis ke seller.
Arsitektur Payment untuk Marketplace
Untuk marketplace produk digital, arsitektur payment memiliki kompleksitas tambahan: uang dari buyer harus split antara platform (komisi) dan seller (revenue). Berikut arsitektur yang recommended:
Flow Transaksi
- Buyer checkout: Pilih produk → pilih payment method → create payment di gateway
- Payment processing: Gateway memproses pembayaran (VA, QRIS, e-wallet)
- Webhook notification: Gateway mengirim webhook ke server Anda saat pembayaran berhasil
- Order fulfillment: Server memproses order — kirim link download ke buyer, notifikasi ke seller
- Settlement: Gateway mengirim uang ke rekening platform (T+1 atau T+2)
- Disbursement: Platform mengirim bagian seller ke rekening seller (menggunakan Xendit Disbursement API atau manual transfer)
Implementasi Midtrans (Snap)
Midtrans Snap adalah cara termudah untuk menerima pembayaran. Buyer diarahkan ke halaman pembayaran Midtrans yang sudah handle semua payment method.
Backend (PHP/Symfony)
// Install SDK
// composer require midtrans/midtrans-php
use Midtrans\Config;
use Midtrans\Snap;
class PaymentController extends AbstractController
{
#[Route('/checkout/{productId}', methods: ['POST'])]
public function checkout(int $productId): JsonResponse
{
// Konfigurasi Midtrans
Config::$serverKey = $this->getParameter('midtrans_server_key');
Config::$isProduction = true;
Config::$isSanitized = true;
Config::$is3ds = true;
$product = $this->productRepo->find($productId);
$orderId = 'ORD-' . time() . '-' . random_int(1000, 9999);
$params = [
'transaction_details' => [
'order_id' => $orderId,
'gross_amount' => $product->getPrice(),
],
'customer_details' => [
'first_name' => $this->getUser()->getName(),
'email' => $this->getUser()->getEmail(),
],
'item_details' => [[
'id' => $product->getId(),
'price' => $product->getPrice(),
'quantity' => 1,
'name' => substr($product->getName(), 0, 50),
]],
'callbacks' => [
'finish' => $this->generateUrl('order_finish',
[], UrlGeneratorInterface::ABSOLUTE_URL),
],
];
$snapToken = Snap::getSnapToken($params);
// Simpan order ke database
$order = new Order();
$order->setOrderId($orderId);
$order->setProduct($product);
$order->setBuyer($this->getUser());
$order->setAmount($product->getPrice());
$order->setStatus('pending');
$order->setSnapToken($snapToken);
$this->em->persist($order);
$this->em->flush();
return $this->json(['snap_token' => $snapToken]);
}
}
Frontend (JavaScript)
// Di halaman checkout
async function checkout(productId) {
const response = await fetch(`/checkout/${productId}`, {
method: 'POST',
headers: { 'X-Requested-With': 'XMLHttpRequest' }
});
const data = await response.json();
// Tampilkan popup pembayaran Midtrans
snap.pay(data.snap_token, {
onSuccess: function(result) {
window.location.href = '/order/success/' + result.order_id;
},
onPending: function(result) {
window.location.href = '/order/pending/' + result.order_id;
},
onError: function(result) {
alert('Pembayaran gagal. Silakan coba lagi.');
}
});
}
Webhook Handler
Ini adalah bagian paling kritial — webhook memastikan order diproses meskipun buyer menutup browser setelah bayar:
#[Route('/webhook/midtrans', methods: ['POST'])]
public function midtransWebhook(Request $request): Response
{
$serverKey = $this->getParameter('midtrans_server_key');
$payload = json_decode($request->getContent(), true);
// Verifikasi signature
$signatureKey = hash('sha512',
$payload['order_id'] .
$payload['status_code'] .
$payload['gross_amount'] .
$serverKey
);
if ($signatureKey !== $payload['signature_key']) {
return new Response('Invalid signature', 403);
}
$order = $this->orderRepo->findByOrderId($payload['order_id']);
if (!$order) {
return new Response('Order not found', 404);
}
// Update status berdasarkan transaction_status
switch ($payload['transaction_status']) {
case 'capture':
case 'settlement':
$order->setStatus('paid');
$order->setPaidAt(new \DateTime());
// Kirim produk ke buyer
$this->fulfillOrder($order);
break;
case 'expire':
case 'cancel':
$order->setStatus('cancelled');
break;
case 'deny':
$order->setStatus('denied');
break;
}
$this->em->flush();
return new Response('OK', 200);
}
Implementasi Xendit
Xendit menggunakan pendekatan yang lebih RESTful. Berikut contoh integrasi untuk pembuatan invoice:
Create Invoice
use GuzzleHttp\Client;
public function createXenditInvoice(Product $product, User $buyer): array
{
$client = new Client([
'base_uri' => 'https://api.xendit.co/',
'auth' => [$this->getParameter('xendit_secret_key'), ''],
]);
$invoiceId = 'INV-' . time() . '-' . random_int(1000, 9999);
$response = $client->post('v2/invoices', [
'json' => [
'external_id' => $invoiceId,
'amount' => $product->getPrice(),
'payer_email' => $buyer->getEmail(),
'description' => 'Pembelian: ' . $product->getName(),
'invoice_duration' => 86400, // 24 jam
'currency' => 'IDR',
'success_redirect_url' => 'https://yoursite.com/order/success',
'failure_redirect_url' => 'https://yoursite.com/order/failed',
'payment_methods' => ['QRIS', 'BCA', 'BNI', 'MANDIRI', 'OVO', 'DANA'],
'items' => [[
'name' => $product->getName(),
'quantity' => 1,
'price' => $product->getPrice(),
]],
],
]);
return json_decode($response->getBody(), true);
// Response berisi 'invoice_url' - redirect buyer ke URL ini
}
Tips Handling Webhook
Webhook adalah area yang paling sering bermasalah dalam integrasi payment gateway. Berikut best practices:
- Idempotency: Webhook bisa dikirim berkali-kali (retry mechanism). Pastikan handler Anda idempotent — memproses webhook yang sama 2x tidak boleh menghasilkan 2x fulfillment.
- Verifikasi signature: Selalu verifikasi signature/token dari webhook. Jangan pernah percaya payload tanpa verifikasi — siapapun bisa mengirim POST request ke webhook URL Anda.
- Response cepat: Return HTTP 200 ASAP. Jangan lakukan processing berat di webhook handler. Dispatch ke queue (Symfony Messenger/Laravel Queue) untuk processing async.
- Logging: Log setiap webhook yang masuk — payload, signature verification result, processing result. Ini invaluable untuk debugging masalah pembayaran.
- Retry handling: Jika webhook handler Anda return non-2xx, gateway akan retry. Midtrans retry 5x, Xendit retry 3x. Pastikan server Anda bisa handle retry tanpa side effect.
Dual Gateway Strategy
Untuk production, kami merekomendasikan menggunakan kedua gateway sekaligus:
- Midtrans sebagai primary: untuk GoPay, ShopeePay, dan QRIS (payment method yang paling sering dipakai buyer Indonesia)
- Xendit sebagai secondary + disbursement: untuk VA transfer dan disbursement otomatis ke seller
Implementasi abstraction layer di code Anda sehingga switch antara gateway tidak mengubah business logic:
interface PaymentGatewayInterface
{
public function createPayment(Order $order): PaymentResult;
public function handleWebhook(array $payload): WebhookResult;
public function refund(Order $order, float $amount): RefundResult;
}
class MidtransGateway implements PaymentGatewayInterface { /* ... */ }
class XenditGateway implements PaymentGatewayInterface { /* ... */ }
Testing Payment Integration
Keduanya menyediakan sandbox/test environment:
- Midtrans Sandbox:
app.sandbox.midtrans.com— gunakan test card number dan VA simulasi - Xendit Test Mode: Gunakan API key dengan prefix
xnd_development_
Buat automated test untuk setiap payment flow: create → pending → paid → fulfilled. Jangan hanya test happy path — test juga expired, cancelled, dan refund scenario. Ini akan menghemat banyak waktu debugging di production.
Payment gateway integration yang solid adalah fondasi dari marketplace yang dipercaya buyer dan seller. Investasikan waktu untuk mengimplementasikannya dengan benar — error di payment = kehilangan uang dan kepercayaan pelanggan. Start dengan Midtrans Snap untuk kecepatan integrasi, tambahkan Xendit untuk disbursement ketika marketplace Anda sudah mulai punya banyak seller.