Cara Integrasikan Payment Gateway di Aplikasi (Midtrans/Xendit)

Transaksi pembayaran digital di aplikasi mobile

Written by

in

Pertama kali saya integrasikan payment gateway, saya takut salah. Ini bukan bug biasa yang bisa di-rollback — ini uang sungguhan, transaksi nyata, customer yang bisa komplain.

Rasa takut itu wajar. Tapi setelah beberapa kali integrasi, saya bisa bilang: lebih mudah dari yang terlihat, selama kamu tahu apa yang dilakukan dan tidak skip langkah testing.

Di Indonesia, ada dua pemain utama yang paling sering dipakai: Midtrans dan Xendit. Keduanya sudah mature, punya dokumentasi yang baik, dan SDK yang tersedia untuk berbagai bahasa. Di artikel ini saya akan bahas keduanya — plus tips praktis dari pengalaman langsung.

Midtrans vs Xendit: Pilih yang Mana?

Sebelum mulai, penting untuk tahu perbedaan keduanya agar tidak salah pilih:

Midtrans (sekarang bagian dari Gojek ekosistem):

  • Lebih lama di pasar Indonesia, database merchant lebih besar
  • Snap (UI pembayaran bawaan) yang polished dan mudah diintegrasikan
  • Support luas untuk metode pembayaran lokal: GoPay, OVO, DANA, VA bank, Indomaret/Alfamart
  • Cocok untuk aplikasi yang butuh minimal setup dan UX yang sudah terbukti

Xendit:

  • Lebih developer-friendly dari sisi API design
  • Support untuk disbursement (kirim uang) yang lebih robust
  • Bisa handle multiple currency lebih baik untuk bisnis regional
  • Cocok untuk fintech, platform dengan kebutuhan payout, atau yang butuh API yang lebih fleksibel

Untuk mayoritas e-commerce atau SaaS dengan user Indonesia: Midtrans cukup dan lebih simpel. Untuk platform yang butuh fitur disbursement atau regional expansion: Xendit lebih cocok.

Integrasi Midtrans: Step by Step

Setup Akun dan Konfigurasi

  1. Daftar di midtrans.com
  2. Masuk ke Dashboard → pilih environment Sandbox untuk development
  3. Ambil Server Key dan Client Key dari Settings → Access Keys

Simpan di environment variables, jangan hardcode di kode:

MIDTRANS_SERVER_KEY=SB-Mid-server-xxxxxxxxxx

MIDTRANS_CLIENT_KEY=SB-Mid-client-xxxxxxxxxx

MIDTRANS_IS_PRODUCTION=false

Instalasi SDK

# Node.js

npm install midtrans-client

Python

pip install midtransclient

PHP

composer require midtrans/midtrans-php

Buat Transaksi (Server-Side)

Contoh dengan Node.js:

const midtransClient = require('midtrans-client');

const snap = new midtransClient.Snap({

isProduction: process.env.MIDTRANS_IS_PRODUCTION === 'true',

serverKey: process.env.MIDTRANS_SERVER_KEY

});

async function createTransaction(orderId, amount, customerDetails) {

const parameter = {

transaction_details: {

order_id: orderId, // ID unik per transaksi

gross_amount: amount // dalam Rupiah, integer

},

customer_details: {

first_name: customerDetails.name,

email: customerDetails.email,

phone: customerDetails.phone

},

item_details: customerDetails.items

};

try {

const transaction = await snap.createTransaction(parameter);

return {

token: transaction.token,

redirect_url: transaction.redirect_url

};

} catch (error) {

throw new Error(Midtrans error: ${error.message});

}

}

Tampilkan Payment UI (Client-Side)

Midtrans Snap menyediakan popup UI — kamu hanya perlu load script dan panggil satu function:

<script src="https://app.sandbox.midtrans.com/snap/snap.js"

data-client-key="YOUR_CLIENT_KEY"></script>

<button onclick="bayar()">Bayar Sekarang</button>

<script>

async function bayar() {

// Minta token dari backend kamu

const response = await fetch('/api/create-transaction', {

method: 'POST',

body: JSON.stringify({ orderId: 'ORDER-001', amount: 150000 })

});

const { token } = await response.json();

// Tampilkan payment popup

window.snap.pay(token, {

onSuccess: function(result) {

console.log('Sukses:', result);

window.location.href = '/success?order=' + result.order_id;

},

onPending: function(result) {

console.log('Pending:', result);

window.location.href = '/pending';

},

onError: function(result) {

console.error('Error:', result);

alert('Pembayaran gagal');

},

onClose: function() {

console.log('Popup ditutup user');

}

});

}

</script>

Handle Webhook (Notification)

Ini bagian yang paling penting dan paling sering di-skip: webhook handler. Midtrans akan mengirim notifikasi ke endpoint kamu setiap kali status transaksi berubah.

app.post('/api/midtrans-webhook', async (req, res) => {

const notification = req.body;

// Verifikasi signature — WAJIB!

const snap = new midtransClient.Snap({

isProduction: false,

serverKey: process.env.MIDTRANS_SERVER_KEY

});

const statusResponse = await snap.transaction.notification(notification);

const orderId = statusResponse.order_id;

const transactionStatus = statusResponse.transaction_status;

const fraudStatus = statusResponse.fraud_status;

if (transactionStatus === 'capture' && fraudStatus === 'accept') {

await updateOrderStatus(orderId, 'paid');

} else if (transactionStatus === 'settlement') {

await updateOrderStatus(orderId, 'paid');

} else if (transactionStatus === 'cancel' || transactionStatus === 'deny' || transactionStatus === 'expire') {

await updateOrderStatus(orderId, 'cancelled');

}

res.status(200).send('OK');

});

Perhatian: Jangan update status order hanya berdasarkan callback dari client-side (snap.pay onSuccess). User bisa manipulasi ini. Selalu andalkan webhook untuk update status final.

Integrasi Xendit: Pendekatan yang Lebih Fleksibel

Xendit lebih cocok kalau kamu mau kontrol lebih atas payment flow. Contoh membuat invoice (link pembayaran):

const { Xendit } = require('xendit-node');

const xendit = new Xendit({ secretKey: process.env.XENDIT_SECRET_KEY });

const { Invoice } = xendit;

async function createXenditInvoice(orderId, amount, customerEmail) {

const invoice = await Invoice.createInvoice({

data: {

external_id: orderId,

amount: amount,

payer_email: customerEmail,

description: Pembayaran Order ${orderId},

success_redirect_url: https://yourdomain.com/success,

failure_redirect_url: https://yourdomain.com/failed,

}

});

return invoice.invoice_url; // URL yang dikirim ke customer

}

Xendit juga punya webhook yang perlu dihandle:

app.post('/api/xendit-webhook', (req, res) => {

// Verifikasi dengan callback token

const callbackToken = req.headers['x-callback-token'];

if (callbackToken !== process.env.XENDIT_CALLBACK_TOKEN) {

return res.status(403).send('Unauthorized');

}

const { external_id, status } = req.body;

if (status === 'PAID') {

updateOrderStatus(external_id, 'paid');

}

res.status(200).send('OK');

});

Tips Kritis yang Sering Diabaikan

Selalu Validasi di Server-Side

Client bisa berbohong. Validasi payment status selalu dari webhook server-side, bukan dari callback client.

Buat Order ID yang Unik dan Idempotent

Format yang baik: ORDER-{timestamp}-{random}. Pastikan idempotent — kalau transaksi sama disubmit dua kali, tidak boleh charge dua kali.

Log Semua Transaksi

Simpan semua webhook payload ke database untuk audit trail. Kalau ada dispute, kamu butuh bukti lengkap.

await db.transactionLogs.create({

orderId,

provider: 'midtrans',

status: transactionStatus,

rawPayload: JSON.stringify(notification),

createdAt: new Date()

});

Test dengan Akun Sandbox yang Lengkap

Midtrans dan Xendit punya sandbox environment dengan test credentials. Gunakan ini untuk test semua skenario: pembayaran sukses, pending, expired, fraud — jangan hanya test happy path.

Setup Alert untuk Transaksi yang Aneh

Transaksi dengan amount sangat besar, atau lonjakan transaksi tiba-tiba, butuh notifikasi ke kamu. Setup alert sederhana ke Slack atau email untuk edge cases ini.

Checklist Sebelum Go Live

Sebelum switch ke production, pastikan:

  • [ ] Server Key dan Client Key sudah ganti ke production keys
  • [ ] isProduction: true di konfigurasi
  • [ ] Webhook URL sudah di-register di dashboard dengan URL production
  • [ ] Test payment sukses di production dengan nominal kecil (Rp 1.000)
  • [ ] Error handling untuk semua skenario sudah diimplementasikan
  • [ ] Log transaksi sudah berjalan
  • [ ] Refund flow sudah di-test

Butuh bantuan integrasi payment gateway untuk aplikasi atau toko online kamu? Yuk ngobrol — hubungi mafadev dan kita kerjakan dengan benar dari awal.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *