Daftar isi

Webhook

TagihanDigital mengirim notifikasi POST ke URL Anda begitu ada peristiwa pembayaran, supaya sistem Anda tidak perlu menanyai API terus-menerus.

Memasang URL webhook

Masuk ke Dashboard, buka Integrasi API, lalu pilih tab Webhook. Isi URL produksi Anda — harus HTTPS dan bisa diakses publik — lalu tekan Kirim tes untuk memastikan endpoint Anda menerima dan membalas dengan benar sebelum dipakai untuk trafik sungguhan.

Event yang dikirim

Eventstatus_codetransaction_statusgross_amount
invoice.paid200settlementTotal invoice
payment.received201partialNominal pembayaran itu
payment_link.expired202expireNominal link

Contoh payload

Berikut bentuk lengkap yang benar-benar dikirim untuk event invoice.paid. Field external_id berisi ID transaksi milik Anda sendiri, persis seperti yang dikirim saat membuat invoice.

{
  "event": "invoice.paid",
  "delivery_id": "whd_3k2j8f",
  "sent_at": "2026-09-16T12:03:11.000Z",
  "test": false,
  "invoice_id": "clx9a",
  "invoice_number": "INV-2026-0012",
  "external_id": "ORD-2026-0912",
  "status_code": "200",
  "transaction_status": "settlement",
  "gross_amount": "150000.00",
  "amount_paid": "150000.00",
  "balance_due": "0.00",
  "payment_status": "PAID",
  "customer": { "id": "cus_123", "name": "PT Maju", "email": "finance@maju.test" },
  "payment": {
    "id": "pay_1",
    "amount": "150000.00",
    "method": "DUITKU_VA",
    "reference": "D1871800123",
    "paid_at": "2026-09-16T12:03:09.000Z"
  },
  "signature_key": "a1f4..."
}

Verifikasi signature

Setiap kiriman membawa signature_key yang dihitung dari tiga field payload ditambah Server Key organisasi Anda. Hitung ulang nilainya di sisi Anda dan bandingkan sebelum memproses kiriman:

signature_key = sha512(invoice_number + status_code + gross_amount + ServerKey)

Hasilnya berupa hex huruf kecil. gross_amount selalu memakai dua desimal (misalnya 150000.00, bukan 150000) — salin nilainya dari payload apa adanya, jangan diformat ulang, karena string yang berbeda menghasilkan signature yang berbeda pula.

Verifikasi signature (PHP):

$body = json_decode(file_get_contents('php://input'), true);
$expected = hash('sha512',
  $body['invoice_number'] . $body['status_code'] . $body['gross_amount'] . $serverKey
);
if (!hash_equals($expected, $body['signature_key'])) {
  http_response_code(403);
  exit;
}
http_response_code(200);

Verifikasi signature (Node.js):

import { createHash, timingSafeEqual } from "node:crypto";

app.post("/webhook/tagihandigital", (req, res) => {
  const b = req.body;
  const expected = createHash("sha512")
    .update(b.invoice_number + b.status_code + b.gross_amount + SERVER_KEY)
    .digest("hex");
  const ok = timingSafeEqual(Buffer.from(expected), Buffer.from(b.signature_key));
  if (!ok) return res.sendStatus(403);

  // delivery_id konstan di seluruh percobaan ulang: simpan dan tolak duplikat.
  res.sendStatus(200);
});

Header tiap kiriman

Setiap request webhook membawa header berikut:

  • X-TD-Event — nama event, misalnya invoice.paid.
  • X-TD-Delivery-Id — sama dengan delivery_id pada payload.
  • X-TD-Testtrue untuk kiriman tes dari Dashboard, false untuk kiriman sungguhan.
  • User-Agent: TagihanDigital-Webhook/1

Jadwal retry

Percobaan pertama dikirim langsung saat peristiwa terjadi. Kalau gagal — respons bukan 2xx, timeout, atau koneksi putus — dicoba ulang dengan jeda +1 menit, +5 menit, +30 menit, +2 jam, lalu +6 jam. Setelah percobaan keenam tetap gagal, statusnya menjadi FAILED dan pengiriman berhenti — Anda bisa mengirim ulang secara manual dari Dashboard.

Anjuran menangani webhook

Balas 200 secepatnya begitu payload diterima dan signature-nya valid, lalu proses isinya (update status, kirim notifikasi, dan seterusnya) di latar belakang — jangan menahan respons sampai proses panjang selesai, supaya tidak dianggap timeout dan dikirim ulang tanpa perlu. Pakai delivery_id sebagai kunci idempotensi: nilainya sama di seluruh percobaan ulang untuk peristiwa yang sama, jadi Anda bisa menolak payload dengan delivery_id yang sudah pernah diproses.