Verifikasi signature webhook
Webhook memberi tahu Anda saat akses yang diberikan pengguna berubah — terutama ketika mereka menghapus akun Tracive padahal aplikasi Anda masih terhubung.
Webhook hanya berjalan di production. Di sandbox field URL-nya terkunci dan tidak ada event yang dicatat maupun dikirim — jadi alur di halaman ini baru bisa Anda uji setelah organisasi Anda disetujui untuk production.
Event yang dikirim
| grant.revoked | Grant berakhir: pengguna mencabut akses, client dinonaktifkan, organisasi dihapus, atau akun Tracive yang sama menghubungkan partner_user_id lain (tautan partner_user_id lama berakhir). |
| grant.superseded | Consent baru dengan partner_user_id sama, akun Tracive berbeda. Pengguna yang menghubungkan ulang akunnya sendiri dengan partner_user_id yang sama tidak memicu event apa pun; token barunya menggantikan yang lama. |
| account.deleted | Pengguna menghapus akun Tracive. Dikirim ke setiap partner terdampak. |
{
"id": "evt_01J0…",
"type": "grant.revoked",
"createdAt": "2026-09-07T04:12:00Z",
"data": {
"sub": "CpPL1akJ9DF-7-bMPkzZyLD6oQBW1iNAtOG3jMoWggA",
"partnerUserId": "cust-8842-xyz",
"clientId": "trcvc_…"
}
}Header signature
X-Tracive-Signature: t=<unix>,v1=HMAC-SHA256(secret, "<t>.<body>")Timestamp ikut ditandatangani, bukan hanya dilampirkan. Kalau ia di luar tanda tangan, penyerang bisa mengganti timestamp pada permintaan yang direkam dan memutarnya ulang kapan saja.
Memverifikasinya
Tanda tangani badan mentah, persis seperti yang tiba. Mem-parse JSON lalu men-serialisasi ulang akan mengubah spasi atau urutan kunci, dan tanda tangan yang sah akan gagal.
import { createHmac, timingSafeEqual } from "node:crypto"
export function verify(rawBody, header, secret) {
// "t=1788764038,v1=9f1c…"
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")))
const t = Number(parts.t)
if (!Number.isFinite(t)) return false
// Tolak yang terlalu tua: signature yang sah tetap bisa diputar ulang
// selamanya kalau usianya tidak pernah diperiksa.
if (Math.abs(Date.now() / 1000 - t) > 300) return false
const expected = createHmac("sha256", secret)
.update(`${t}.${rawBody}`)
.digest("hex")
const a = Buffer.from(expected)
const b = Buffer.from(parts.v1 ?? "")
// Panjang harus dicek dulu: timingSafeEqual melempar kalau berbeda.
return a.length === b.length && timingSafeEqual(a, b)
}- · Bandingkan dengan perbandingan waktu-tetap, bukan
==. - · Tolak timestamp yang terlalu tua. Lima menit adalah jendela yang wajar.
- · Jawab 2xx secepatnya, lalu kerjakan di belakang layar.
Pengiriman dan retry
Pengiriman bersifat at-least-once, dengan hingga sembilan percobaan berjarak 30 detik sampai 1 jam — sekitar dua jam. Artinya event yang sama bisa tiba dua kali.
Setiap payload membawa
id yang unik. Simpan id yang sudah diproses dan abaikan pengulangannya — itu satu-satunya cara aman menangani at-least-once.Client tanpa URL webhook tidak menghambat apa pun: event tetap dicatat, hanya tidak ada yang dikirim.
Secret dan rotasinya
Secret ditampilkan sekali saat Anda memasang URL webhook. Memasang URL lagi akan merotasi secretnya.
Tidak ada jendela dua-secret. Merotasi tanpa men-deploy nilai barunya akan menggagalkan verifikasi pada event yang sedang di jalan. Delapan kali retry biasanya melewati satu deploy, dan event yang terlewat tetap bisa dipulihkan dengan membaca ulang status grant.