Webhook / Callback
BITS Pay mengirim HTTP POST ke callback_url aplikasi kamu setiap status
pembayaran berubah. Tidak perlu polling.
Setup
Isi callback_url saat membuat aplikasi di dashboard. BITS Pay menyimpan
callback_secret per aplikasi untuk menandatangani setiap payload.
callback_allowed di tier features). Tier free
tidak mendapat callback — cek tier kamu di dashboard.
Uji endpoint kamu kapan saja dengan POST /v1/callbacks/test (cukup header API
key, tanpa body). BITS Pay mengirim event callback.test bertanda tangan penuh
ke callback_url aplikasi dan melaporkan status HTTP yang diterima — cocok untuk
memvalidasi verifikasi signature sebelum transaksi pertama.
Request yang dikirim
POST {callback_url}
Content-Type: application/json
X-BITS-Signature: <hmac-sha256-hex>
X-BITS-Timestamp: <unix-seconds>
X-BITS-Event: payment.success
Payload JSON:
{
"event": "payment.success",
"transaction": {
"id": "9b1f2c3e-...",
"order_id": "ORD-001",
"amount": 150000,
"fee": 1000,
"amount_due": 151001,
"status": "success",
"paid_at": "2026-09-02T12:32:00Z"
}
}
Event
| Event | Kapan dikirim |
|---|---|
payment.success |
Pembayaran terkonfirmasi (OCR auto / manual admin) |
payment.failed |
Pembayaran gagal / ditolak |
payment.expired |
Transaksi melewati expired_at tanpa pembayaran |
Verifikasi signature
X-BITS-Signature = HMAC-SHA256 hex dari string
"{X-BITS-Timestamp}.{raw body}", dengan key
callback_secret aplikasi kamu. Wajib verifikasi signature
dan timestamp (toleransi 5 menit) sebelum memproses payload.
// Node.js 18+ / Cloudflare Workers (Web Crypto)
async function verify(rawBody, signature, timestamp, secret) {
if (!signature || !/^[0-9a-f]{64}$/.test(signature)) return false;
// Anti-replay: tolak pesan lebih tua dari 5 menit.
const age = Math.abs(Math.floor(Date.now() / 1000) - Number(timestamp));
if (!Number.isFinite(age) || age > 300) return false;
const enc = new TextEncoder();
const key = await crypto.subtle.importKey(
'raw',
enc.encode(secret),
{ name: 'HMAC', hash: 'SHA-256' },
false,
['verify'],
);
const sigBytes = new Uint8Array(signature.match(/.{2}/g).map((h) => parseInt(h, 16)));
// Signature mencakup timestamp: HMAC(secret, "{timestamp}.{body}").
// crypto.subtle.verify membandingkan MAC secara constant-time — aman dari
// timing attack. Jangan bandingkan hex dengan === (bocor via timing).
return crypto.subtle.verify('HMAC', key, sigBytes, enc.encode(`${timestamp}.${rawBody}`));
}
// Di handler webhook:
const rawBody = await request.text(); // PENTING: raw string, bukan JSON.parse dulu
const signature = request.headers.get('X-BITS-Signature');
const timestamp = request.headers.get('X-BITS-Timestamp');
if (!(await verify(rawBody, signature, timestamp, CALLBACK_SECRET))) {
return new Response('invalid signature', { status: 401 });
}
const payload = JSON.parse(rawBody);
Retry
Jika endpoint kamu tidak merespons sukses (HTTP 2xx), callback masuk antrian retry dengan
backoff sampai max_attempts (per tier). Balas cepat dengan 2xx, proses berat
lakukan async. Handler kamu harus idempoten — event yang sama bisa dikirim
ulang.