← Docs

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.

Fitur callback tunduk pada tier (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);
Signature dihitung dari timestamp + body mentah apa adanya. Jangan re-serialize JSON sebelum verifikasi — urutan key/spasi berbeda akan mengubah hash. Timestamp mencegah replay: pesan lama yang dikirim ulang wajib ditolak.

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.