Home Blog Series B B02
Series B: System Design for Indonesian Developers · B02 Indonesia Tech 8 September 2025 9 min read

Bagaimana Midtrans Payment Flow Benar-Benar Bekerja

5 pitfall yang tidak ada di dokumentasi resmi — dari webhook validation, idempotency, settlement timing, sampai PPh 23 untuk transaksi B2B.

RM
Ryan Muliadi
Digital Systems Architect · Ryzen Digital Systems

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

Perbedaan Fundamental
Snap = Midtrans yang mengelola UI pembayaran. Core API = Anda yang bangun UI sendiri, punya kontrol penuh. Untuk kebanyakan startup dan UKM, Snap lebih cepat dan aman. Core API untuk kasus yang butuh UX custom.
AspekSnapCore API
Implementasi1–2 hari1–2 minggu
PCI DSS complianceDihandle MidtransTanggung jawab Anda
UI Customization△ Terbatas (theme saja)Full control
Payment method baruOtomatis tersediaPerlu update integrasi
Mobile experience△ Popup/redirectNative in-app flow
Cocok untukStartup, UKM, MVPMarketplace besar, fintech

Flow Pembayaran yang Sebenarnya

Ini yang tidak ada di dokumentasi resmi — sequence lengkap dari klik "Bayar" sampai order confirmed:

Midtrans Snap Payment Flow — Happy Path
BROWSER BACKEND MIDTRANS BANK / GOPAY 1. Klik Bayar → POST /create-transaction 2. POST /snap/v1/transactions (server-to-server) 3. Return snap_token 4. Return snap_token ke browser 5. snap.pay(token) → tampilkan popup Midtrans 6. User pilih metode + konfirmasi bank 7. WEBHOOK → POST /payment/notification 8. Update status order + trigger fulfillment ⚠ Step 7 adalah satu-satunya sumber kebenaran — bukan callback JS step 5

5 Pitfall Webhook yang Sering Terjadi

01
Percaya callback JavaScript, bukan webhook
Callback JS dari Snap (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.
02
Tidak validasi signature key
Webhook harus divalidasi dengan 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.
03
Tidak idempotent — proses duplikat
Midtrans bisa mengirim webhook yang sama beberapa kali (retry jika tidak ada respons 200 dalam 5 detik). Tanpa idempotency check, fulfillment bisa diproses dua kali. Simpan transaction_id dan check sebelum proses.
04
Webhook URL tidak accessible dari internet
Saat development lokal, Midtrans tidak bisa reach localhost:3000. Pakai ngrok atau Cloudflare Tunnel untuk expose local endpoint. Di production, pastikan tidak ada firewall yang block Midtrans IP range.
05
Settlement timing tidak diperhitungkan
Transfer bank biasa T+1 sampai T+2 hari kerja setelah transaction. BI FAST lebih cepat tapi tidak semua bank support. Pastikan logika akuntansi mempertimbangkan settlement date, bukan transaction date.

Implementasi Webhook yang Benar

TypeScript · Next.js API Route
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
}
⚠️ PPh 23 di Invoice Klien Korporat

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.