Saya pernah mendapat panggilan darurat pukul 11 malam dari klien: "Payment sudah berhasil di frontend tapi status order masih PENDING — kenapa?"
Jawabannya ada di webhook. Tapi bukan hanya soal webhook tidak terpasang — ada 5 layer yang perlu dipahami agar Midtrans integration benar-benar production-ready.
Snap vs Core API — Pilih yang Tepat
| Aspek | Snap | Core API |
|---|---|---|
| Implementasi | 1–2 hari | 1–2 minggu |
| PCI DSS compliance | Dihandle Midtrans | Tanggung jawab Anda |
| UI Customization | △ Terbatas (theme saja) | Full control |
| Payment method baru | Otomatis tersedia | Perlu update integrasi |
| Mobile experience | △ Popup/redirect | Native in-app flow |
| Cocok untuk | Startup, UKM, MVP | Marketplace besar, fintech |
Flow Pembayaran yang Sebenarnya
Ini yang tidak ada di dokumentasi resmi — sequence lengkap dari klik "Bayar" sampai order confirmed:
5 Pitfall Webhook yang Sering Terjadi
onSuccess) bisa di-spoof oleh user yang technical. Satu-satunya sumber kebenaran adalah webhook server-to-server dari Midtrans ke backend Anda. Selalu update status order berdasarkan webhook, bukan JS callback.SHA512(order_id + status_code + gross_amount + server_key) yang di-compare dengan signature_key dari payload. Tanpa ini, siapapun bisa POST ke endpoint Anda dan fake payment success.transaction_id dan check sebelum proses.localhost:3000. Pakai ngrok atau Cloudflare Tunnel untuk expose local endpoint. Di production, pastikan tidak ada firewall yang block Midtrans IP range.Implementasi Webhook yang Benar
import { createHash } from 'crypto'
import { NextRequest, NextResponse } from 'next/server'
export async function POST(req: NextRequest) {
const body = await req.json()
// 1. Validasi signature
const expectedSig = createHash('sha512')
.update(`${body.order_id}${body.status_code}${body.gross_amount}${process.env.MIDTRANS_SERVER_KEY}`)
.digest('hex')
if (body.signature_key !== expectedSig) {
return NextResponse.json({ error: 'Invalid signature' }, { status: 401 })
}
// 2. Idempotency check
const existing = await db.payment.findUnique({
where: { transactionId: body.transaction_id }
})
if (existing?.status === 'settlement') {
return NextResponse.json({ message: 'Already processed' }) // 200 OK, no reprocess
}
// 3. Handle status
if (body.transaction_status === 'settlement' || body.transaction_status === 'capture') {
await db.order.update({
where: { id: body.order_id },
data: { status: 'PAID', paidAt: new Date() }
})
await triggerFulfillment(body.order_id)
} else if (body.transaction_status === 'expire' || body.transaction_status === 'cancel') {
await db.order.update({
where: { id: body.order_id },
data: { status: 'CANCELLED' }
})
}
return NextResponse.json({ ok: true }) // HARUS return 200, atau Midtrans retry
}
Untuk transaksi B2B dengan nilai tertentu, klien korporat wajib memotong PPh 23 (2% untuk jasa teknis) sebelum membayar. Artinya invoice Rp 10 juta akan dibayarkan Rp 9.8 juta. Sertakan kolom "PPh 23 ditanggung pembeli" di template invoice dan komunikasikan sejak proposal.
Kesimpulan
Midtrans Snap adalah pilihan terbaik untuk kebanyakan project Indonesia — cepat diimplementasi dan PCI DSS sudah dihandle. Yang kritis: webhook validation dengan signature key, idempotency untuk mencegah duplikat proses, dan jangan pernah percaya JS callback sebagai konfirmasi pembayaran.
Webhook adalah satu-satunya sumber kebenaran dalam integrasi payment. Apapun yang terjadi di frontend adalah UX — yang terjadi di webhook adalah bisnis.