Webhooks
Webhook = sistem push: gateway POST ke URL lo saat ada event (mis. payment.success). Alternatif paling efisien dari polling.
Setup
- /dashboard/webhooks — paste URL HTTPS endpoint lo (must accept POST).
- Secret HMAC otomatis di-generate (64-char hex). Simpan baik-baik — dipakai untuk verify signature.
Anatomi request
Saat invoice paid, gateway kirim:
POST /your/webhook/path HTTP/1.1 Host: yourapp.com Content-Type: application/json X-Webhook-Event: payment.success X-Webhook-Signature: <sha256_hex_64chars> X-Webhook-Delivery-Id: <uuid> X-Webhook-Attempt: 1 User-Agent: Dehuyz-Gateway-Webhook/1.0 {"event":"payment.success","invoice_id":"abc...","amount":50000,...}
Verifikasi signature (WAJIB)
Gateway tanda-tangan body dengan HMAC-SHA256 + secret merchant. Reject request kalau signature tidak match — kalau gak verify, attacker bisa fake webhook.
Python (FastAPI / Flask)
import hashlib import hmac WEBHOOK_SECRET = "your_secret_dari_dashboard" def verify_webhook(raw_body: bytes, signature: str) -> bool: expected = hmac.new( WEBHOOK_SECRET.encode("utf-8"), raw_body, hashlib.sha256, ).hexdigest() return hmac.compare_digest(expected, signature) # FastAPI from fastapi import FastAPI, Header, Request, HTTPException app = FastAPI() @app.post("/webhook") async def webhook( request: Request, x_webhook_signature: str = Header(...), ): raw = await request.body() if not verify_webhook(raw, x_webhook_signature): raise HTTPException(401, "Invalid signature") payload = await request.json() # process payment... return {"ok": True}
Node.js (Express)
const crypto = require('crypto'); const express = require('express'); const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET; const app = express(); app.post('/webhook', // IMPORTANT: raw body untuk HMAC verify express.raw({ type: 'application/json' }), (req, res) => { const sig = req.headers['x-webhook-signature']; const expected = crypto .createHmac('sha256', WEBHOOK_SECRET) .update(req.body) .digest('hex'); if (!crypto.timingSafeEqual( Buffer.from(expected), Buffer.from(sig), )) { return res.status(401).send('Invalid signature'); } const payload = JSON.parse(req.body.toString()); // process payment... res.json({ ok: true }); } ); app.listen(3000);
PHP
<?php $secret = $_ENV['WEBHOOK_SECRET']; $body = file_get_contents('php://input'); $sig = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? ''; $expected = hash_hmac('sha256', $body, $secret); if (!hash_equals($expected, $sig)) { http_response_code(401); exit('Invalid signature'); } $payload = json_decode($body, true); // process... echo json_encode(['ok' => true]);
Payload payment.success
{ "event": "payment.success", "invoice_id": "a1b2c3d4e5f67890", "external_id": "order-001", "amount": 50000, "fee_amount": 0, "nett_amount": 50000, "refnum_bri": "100000XYZ", "tracenum_bri": "000042", "paid_at": "2026-05-22T12:05:30+00:00", "metadata": {"user_id": "abc"} }
Retry policy
Kalau endpoint lo return non-2xx atau timeout (10s), sistem retry dengan exponential backoff:
| Attempt | Delay |
|---|---|
| 1 | immediate |
| 2 | +5 detik |
| 3 | +30 detik |
| 4 | +5 menit |
| 5 | +30 menit |
| 6 | +2 jam (terakhir) |
Setelah attempt ke-6 gagal, status delivery jadi exhausted. Lihat di /dashboard/webhooks.
Kalau endpoint lo gagal 50x consecutive exhausted, sistem auto-disable endpoint (proteksi loop). Lo harus enable manual.
Idempotency receiver
Webhook bisa di-deliver lebih dari sekali (mis. server lo sempat down lalu BRI poll detect kedua kali, atau retry setelah timeout). Receiver wajib idempotent:
# pakai invoice_id sebagai dedup key async def handle(payload): if await db.fetch_one( "SELECT 1 FROM processed_webhooks WHERE invoice_id = $1", payload["invoice_id"], ): return {"ok": True, "deduped": True} # do work + insert ke processed_webhooks
Atau gunakan X-Webhook-Delivery-Id (UUID per attempt — same delivery, different attempts share same ID).
Test webhook
Klik tombol Test di endpoint Anda di /dashboard/webhooks. Sistem akan kirim payload synthetic payment.success.test (ditandai metadata.test: true) supaya lo bisa cek end-to-end tanpa real payment.
Security notes
- ✅ Selalu pakai HTTPS endpoint
- ✅ Pakai
hmac.compare_digest/crypto.timingSafeEqual(constant-time compare) - ✅ Endpoint dipublish di server lo (jangan ngumpet di vendor cloud function tanpa public DNS)
- ❌ Jangan log full secret di file — hash kalau perlu untuk debug
- ❌ Jangan respond 200 sebelum verify — attacker bisa replay