Dalam ekosistem perdagangan digital dan aplikasi B2B di Indonesia, integrasi sistem pembayaran multi-saluran (QRIS, Virtual Account bank nasional, e-wallet, hingga kartu kredit) umumnya bertumpu pada penyedia gerbang pembayaran (payment gateway) seperti Midtrans dan Xendit.
Meskipun integrasi antarmuka checkout tampak mudah dengan memanfaatkan SDK popup bawaan, tantangan rekayasa perangkat lunak sesungguhnya terletak pada penanganan Webhook Notification (HTTP Callback) di sisi backend.
Jaringan internet publik tidak pernah dapat diandalkan secara sempurna (unreliable network). Server payment gateway dapat mengirimkan satu notifikasi pembayaran yang sama sebanyak tiga atau empat kali akibat timeout sementara pada koneksi HTTP. Jika backend Anda tidak dirancang dengan prinsip Idempotensi dan verifikasi kriptografis yang tangguh, sistem Anda berisiko memproses pesanan ganda, mengisi saldo kredit pengguna dua kali, atau mengalami benturan status transaksi (race condition).
Alur Penanganan Webhook Idempoten Payment Gateway:
[Penyedia Payment Gateway (Midtrans / Xendit)]
│
▼ POST /api/webhooks/payment (Membawa Signature Hash)
┌─────────────────────────────────────────────────────────────┐
│ API Gateway / HTTP Controller │
│ 1. Verifikasi Signature Kriptografis (SHA-512 / HMAC) │
│ 2. Jika Signature Tidak Cocok ────> [Tolak: HTTP 401/403] │
│ 3. Simpan Payload ke Antrean Redis Message Queue │
│ 4. Balas Segera ke Gateway: HTTP 200 OK (< 200 milidetik) │
└──────────────────────────────┬──────────────────────────────┘
│
▼ (Asynchronous Processing)
┌─────────────────────────────────────────────────────────────┐
│ Background Worker (NestJS / BullMQ / Go Worker) │
│ 1. Buka PostgreSQL Transaction dengan Lock Eksklusif │
│ 2. Cek Tabel Idempotency: Apakah Transaction ID sudah ada? │
│ ├── SUDAH PROSES ──> Abaikan Payload Duplikat │
│ └── BELUM PROSES ──> Perbarui Status Pesanan ke PAID │
│ 3. Terbitkan Faktur Pajak & Kirim Email Notifikasi │
└─────────────────────────────────────────────────────────────┘
1. Verifikasi Signature Kriptografis #
Langkah keamanan paling mendasar sebelum membaca isi data transaksi adalah memvalidasi keaslian pengirim notifikasi. Tanpa verifikasi tanda tangan (signature verification), pihak penyerang dapat menembak endpoint webhook Anda secara langsung dan memalsukan status pembayaran menjadi settlement atau PAID.
A. Verifikasi Signature Midtrans (SHA-512) #
Midtrans menyertakan nilai signature_key pada payload notifikasi JSON. Nilai ini dihasilkan dari penggabungan string data spesifik dengan Server Key rahasia:
$$\text{Signature} = \text{SHA512}(\text{order_id} + \text{status_code} + \text{gross_amount} + \text{ServerKey})$$
Implementasi verifikasi pada Node.js:
import crypto from 'crypto';
export function verifyMidtransSignature(payload: {
order_id: string;
status_code: string;
gross_amount: string;
signature_key: string;
}, serverKey: string): boolean {
const rawString = `${payload.order_id}${payload.status_code}${payload.gross_amount}${serverKey}`;
const calculatedHash = crypto.createHash('sha512').update(rawString).digest('hex');
// Gunakan timingSafeEqual untuk mencegah serangan timing-attack
return crypto.timingSafeEqual(
Buffer.from(calculatedHash),
Buffer.from(payload.signature_key)
);
}
B. Verifikasi Webhook Token Xendit #
Xendit mengirimkan token verifikasi pada header HTTP x-callback-token:
export function verifyXenditCallbackToken(
incomingToken: string | undefined,
expectedToken: string
): boolean {
if (!incomingToken) return false;
return crypto.timingSafeEqual(
Buffer.from(incomingToken),
Buffer.from(expectedToken)
);
}
2. Pola Penanganan Asinkron Berkecepatan Tinggi #
Banyak penyedia payment gateway menerapkan batas waktu tunggu respons (HTTP timeout) yang sangat ketat, biasanya antara 5 hingga 10 detik. Jika endpoint backend Anda mencoba memverifikasi pembayaran, memperbarui lima tabel database, memicu pembuatan berkas PDF faktur, dan memanggil API email pihak ketiga dalam satu siklus request HTTP yang sama, koneksi berisiko mengalami timeout.
Kegagalan respons dalam batas waktu tersebut akan memicu algoritma retry otomatis dari gateway, membanjiri server Anda dengan request duplikat.
Aturan emas penanganan webhook: Balas HTTP 200 OK secepat mungkin setelah payload diverifikasi dan disimpan ke antrean antrean pesan (message queue) terisolasi.
// app/api/webhooks/midtrans/route.ts
import { NextResponse } from 'next/server';
import { paymentQueue } from '@/lib/queue';
import { verifyMidtransSignature } from '@/lib/payment-security';
export async function POST(request: Request) {
const payload = await request.json();
// 1. Validasi Tanda Tangan Kriptografis
const isValid = verifyMidtransSignature(payload, process.env.MIDTRANS_SERVER_KEY!);
if (!isValid) {
return NextResponse.json({ error: 'Invalid Signature Key' }, { status: 403 });
}
// 2. Masukkan ke BullMQ Queue untuk diproses di latar belakang
await paymentQueue.add('process_payment_status', payload, {
attempts: 5,
backoff: { type: 'exponential', delay: 2000 },
removeOnComplete: true,
});
// 3. Konfirmasi penerimaan segera ke Payment Gateway
return NextResponse.json({ status: 'OK', message: 'Webhook received' });
}
3. Menjamin Idempotensi dengan Database Locking #
Idempotensi adalah sifat di mana sebuah operasi dapat dieksekusi berulang kali dengan hasil akhir yang sama persis seperti saat pertama kali dijalankan, tanpa efek samping tambahan.
Untuk mencegah race condition saat dua webhook dari gateway yang sama tiba dalam selisih milidetik, backend worker wajib memanfaatkan transaksi database PostgreSQL dengan mekanisme penguncian baris eksplisit (SELECT ... FOR UPDATE):
-- Skema Tabel Idempotency Ledger
CREATE TABLE payment_webhook_logs (
id SERIAL PRIMARY KEY,
transaction_id VARCHAR(100) NOT NULL UNIQUE,
order_id VARCHAR(100) NOT NULL,
payment_status VARCHAR(50) NOT NULL,
payload JSONB NOT NULL,
processed_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
Logika eksekusi di dalam worker:
// workers/payment.worker.ts
import { pool } from '@/lib/db';
export async function processPaymentTransaction(payload: any) {
const client = await pool.connect();
try {
await client.query('BEGIN');
// 1. Kunci baris pesanan untuk mencegah proses paralel
const orderRes = await client.query(
'SELECT id, status, total_amount FROM orders WHERE id = $1 FOR UPDATE',
[payload.order_id]
);
if (orderRes.rows.length === 0) {
await client.query('ROLLBACK');
return;
}
const currentOrder = orderRes.rows[0];
// Jika pesanan sudah berstatus PAID sebelumnya, abaikan webhook ini (Idempoten)
if (currentOrder.status === 'PAID') {
await client.query('ROLLBACK');
console.log(`[Idempotent Skip] Pesanan ${payload.order_id} sudah pernah dibayar.`);
return;
}
// 2. Evaluasi status transaksi dari gateway
if (payload.transaction_status === 'settlement' || payload.transaction_status === 'capture') {
// Perbarui status pesanan menjadi LUNAS
await client.query(
'UPDATE orders SET status = $1, payment_method = $2, paid_at = NOW() WHERE id = $3',
['PAID', payload.payment_type, payload.order_id]
);
// Catat ke log idempotensi
await client.query(
'INSERT INTO payment_webhook_logs (transaction_id, order_id, payment_status, payload) VALUES ($1, $2, $3, $4)',
[payload.transaction_id, payload.order_id, 'PAID', JSON.stringify(payload)]
);
}
await client.query('COMMIT');
} catch (err) {
await client.query('ROLLBACK');
throw err; // Memicu retry otomatis BullMQ
} finally {
client.release();
}
}
4. Mekanisme Rekonsiliasi Cadangan (Fallback Polling Job) #
Meskipun sistem webhook telah dirancang dengan sempurna, terdapat skenario ekstrem di mana penyedia gateway gagal mengirimkan callback akibat gangguan rute routing BGP telekomunikasi.
Sebagai jaring pengaman terakhir, aplikasi enterprise wajib menyertakan Cron Polling Job berkala (misalnya setiap 15 menit):
- Cari seluruh pesanan yang masih berstatus
PENDINGselama lebih dari 30 menit. - Hubungi API pengecekan status Midtrans (
GET /v2/{order_id}/status) atau Xendit (GET /v2/invoices/{invoice_id}). - Jika status di payment gateway telah sukses, perbarui database menggunakan fungsi idempoten yang sama.
Dengan memadukan validasi tanda tangan kriptografis, arsitektur antrean asinkron, penguncian transaksi database idempoten, dan job rekonsiliasi cadangan, sistem penagihan aplikasi Anda akan beroperasi dengan integritas finansial tanpa cela.
