D
Dehuyz Gateway
Docs Webhooks

Webhooks

Webhook = sistem push: gateway POST ke URL lo saat ada event (mis. payment.success). Alternatif paling efisien dari polling.

Setup

  1. /dashboard/webhooks — paste URL HTTPS endpoint lo (must accept POST).
  2. 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