Webhooks ke project
Saat charge menjadi paid, expired, atau failed, Payroute mengirim POST ke callback_url charge itu.
Request
Header:
| Header | Isi |
|---|---|
X-Payroute-Event |
charge.paid / charge.expired / charge.failed |
X-Payroute-Delivery |
id pengiriman, mis. dlv_1842 — sama untuk setiap retry event itu |
X-Payroute-Timestamp |
Unix detik saat dikirim |
X-Payroute-Signature |
HMAC-SHA256 hex dengan secret client Anda |
User-Agent |
VOG-Payroute/1 |
Body:
{
"event": "charge.paid",
"charge": { "id": "PR01JC5Q8ZK3M2N4P6R8T0", "invoice_id": "INV-20261105-0001", "status": "paid", "gateway": "tripay", "rerouted": false, "...": "…" }
}
charge sama persis dengan objek di GET /v1/charges/{id}.
Verifikasi (wajib)
Hitung signature seperti request signature dengan method POST dan path + query callback_url Anda, lalu bandingkan dengan constant-time compare. Tolak (401) kalau tidak cocok atau timestamp terlalu jauh (mis. > 5 menit — retry selalu ditandatangani ulang dengan waktu kirimnya).
// pathAndQuery: path + query of the callback_url you registered (as Payroute
// sent it), e.g. "/payroute/callback" — use r.URL.RequestURI() only when no
// proxy rewrites the path.
func Verify(secret, pathAndQuery string, r *http.Request, body []byte, nowUnix int64) bool {
ts := r.Header.Get("X-Payroute-Timestamp")
sec, err := strconv.ParseInt(ts, 10, 64)
if err != nil || nowUnix-sec > 300 || sec-nowUnix > 300 { // tolak di luar ±5 menit (anti replay)
return false
}
want := Sign(secret, ts, "POST", pathAndQuery, body)
return hmac.Equal([]byte(want), []byte(r.Header.Get("X-Payroute-Signature")))
}
Versi lengkap (Go dan TypeScript) dengan test: docs/examples/sign.go, docs/examples/sign.ts.
Aturan penting
- Jawab 2xx secepatnya (< 10 detik). Proses berat lakukan di belakang.
- At-least-once: event yang sama bisa datang lebih dari sekali. Dedup dengan
X-Payroute-Deliveryatau dengan membuat update Anda idempotent. - Percaya
charge.status, bukan nama event. Body selalu berisi status terkini saat dikirim; retrycharge.expiredyang telat bisa membawastatus: "paid"kalau pelanggan sudah bayar. - Redirect (3xx) tidak diikuti dan dihitung gagal. Gunakan URL final.
- Charge yang gagal saat create (
502) tidak mengirim webhook — Anda sudah tahu dari responsnya.
Retry
Jawaban selain 2xx, timeout > 10 detik, atau koneksi gagal → dicoba lagi setelah 1 menit, 5 menit, 15 menit, 1 jam, 6 jam, 24 jam, 24 jam. Kegagalan ke-8 (± 2 hari 7 jam) menghentikan pengiriman (dead); tim Payroute bisa mengirim ulang. Selama itu, GET /v1/charges/{id} tetap memberi status terbaru.