Webhook CRM terlihat seperti yang paling gampang. Tidak ada uang yang berpindah, tidak ada langganan yang terhapus jika Anda lambat. Sebuah kontak berubah, perbarui salinan Anda.
Lalu seorang pelanggan memberi tahu bahwa lifecycle stage mereka di aplikasi Anda tertulis Lead, sementara di HubSpot tertulis Customer, dan sudah salah selama dua minggu, padahal tidak satu pun event yang terlibat diproses dua kali.
Webhook data bisnis gagal dengan cara yang berbeda dari webhook pembayaran. Volumenya didominasi pembaruan, bukan event baru, objek yang sama berubah berkali-kali dalam waktu singkat, dan jaminan urutannya lebih lemah daripada yang akan diasumsikan siapa pun yang membaca payload-nya.
TL;DR
- “HubSpot tidak menjamin Anda akan menerima notifikasi ini sesuai urutan terjadinya”
- Notifikasi tiba sebagai array, bukan objek tunggal. Satu batch bisa berisi hingga 100.
- Batas waktu responsnya 5 detik, untuk seluruh batch. Pengirim lain ada di referensi provider webhook.
- Notifikasi yang gagal dicoba ulang hingga 10 kali dalam 24 jam, yang memperlebar rentang waktu ketika urutan bisa tertukar.
eventIddidokumentasikan sebagai “tidak dijamin unik”, jadi itu idempotency key yang salah.- Urutkan berdasarkan
occurredAt. Deduplikasi dengan key komposit. Jangan pernah mengurutkan berdasarkan waktu kedatangan.
Field yang tampak seperti jawabannya
Jika terbiasa dengan Stripe, refleksnya adalah mencari ID event, memasang indeks unik di sana, lalu lanjut. Payload HubSpot punya eventId tepat di depan mata.
Dokumentasinya menyatakan: eventId adalah “ID event yang memicu notifikasi ini. Nilai ini tidak dijamin unik.”
Itu peringatan yang luar biasa lugas, dan mudah terlewat saat membaca sekilas. Indeks unik pada eventId akan tampak benar saat pengujian dan, pada suatu saat di production, akan menolak event yang Anda butuhkan atau menerima event yang sudah Anda tangani. Kedua kegagalan itu tidak memberi tanda apa pun.
Dua field yang bisa Anda andalkan adalah occurredAt, “Kapan event ini terjadi, sebagai stempel waktu (timestamp) dalam milidetik”, dan attemptNumber, “Dimulai dari 0, upaya ke berapa ini untuk memberi tahu layanan Anda tentang event ini.”
attemptNumber adalah alat diagnosis yang berguna. Nilai di atas 0 berarti ini percobaan ulang (retry), yang berarti endpoint Anda gagal sebelumnya, yang berarti event kini tiba tidak sesuai urutan terjadinya.
Batching mengubah bentuk handler
HubSpot tidak mengirim satu notifikasi per permintaan: “Anda harus siap menerima array berisi beberapa objek dalam satu permintaan. Ukuran batch bisa bervariasi, tetapi akan di bawah 100 notifikasi.”
Ada tiga konsekuensi.
Handler Anda adalah loop, bukan fungsi. Batas waktu 5 detik mencakup seluruh array. Round trip database per item sebesar 30ms saja sudah melewati batas itu di sekitar 160 item, dan dengan pekerjaan lain pada setiap item, batas itu bisa terlewati sebelum array mencapai 100.
HTTP tidak punya kosakata untuk kegagalan parsial. Jika item ke-40 dari 90 melempar error, Anda tidak bisa memberi tahu HubSpot “39 yang pertama sudah masuk”. Anda mengembalikan satu status untuk seluruh batch. Kembalikan non-2xx, dan semua 90 notifikasi itu datang lagi saat percobaan ulang; kembalikan 200, dan 50 yang belum Anda jangkau hilang.
Itu memaksa kesimpulan yang sama seperti di tempat lain: simpan seluruh array lebih dulu, kembalikan 200, proses sesudahnya. Penulisan batch cukup satu insert, bukan 90, dan itu sekaligus menyelesaikan masalah waktunya.
export async function POST(req: Request) {
const raw = await req.text();
if (!verifyHubSpotSignature(raw, req.headers)) {
return new Response("unauthorized", { status: 401 });
}
const notifications = JSON.parse(raw); // always an array
await db.insertBatch(notifications); // one durable write
return new Response(null, { status: 200 }); // inside 5s regardless of size
}
Mengapa urutan kacau, dan mengapa deduplikasi tidak bisa memperbaikinya
Seorang sales mengedit deal empat kali dalam sembilan puluh detik. Empat notifikasi. Endpoint Anda sempat tidak sehat dan menolak notifikasi kedua. HubSpot mencobanya ulang, “hingga 10 kali”, dengan percobaan ulang yang “tersebar selama 24 jam berikutnya, dengan jeda yang bervariasi antarpermintaan.”
Jadi notifikasi kedua tiba setelah notifikasi ketiga dan keempat. Handler Anda menerapkannya. Stage deal itu kini kembali ke nilai saat edit kedua, dan akan tetap begitu sampai seseorang menyentuh record itu lagi.
Tidak ada notifikasi yang diproses dua kali. Deduplikasi bekerja sempurna. Datanya tetap salah.
Inilah perbedaan yang penting: idempotensi melindungi dari memproses event yang sama dua kali. Tidak ada bagian darinya yang melindungi dari menerapkan state lama di atas state yang lebih baru. Keduanya kegagalan yang berbeda dengan perbaikan yang berbeda, dan aliran event CRM jauh lebih sering menghasilkan kegagalan kedua dibanding aliran event pembayaran, karena objek yang sama diperbarui berulang kali.
Perbaikannya adalah guard monotonik di dalam operasi penulisannya sendiri.
UPDATE contacts
SET lifecycle_stage = $1, hs_updated_at = $2
WHERE hubspot_id = $3
AND hs_updated_at < $2; -- refuse to apply stale state
Gunakan occurredAt untuk $2, bukan NOW() milik Anda sendiri. Jam Anda mencatat kapan Anda menerimanya, dan urutan penerimaan justru hal yang tidak bisa diandalkan. Pembahasan yang lebih luas, termasuk apa yang harus dilakukan untuk state machine dengan invarian sungguhan, ada di Anda Menerima Webhook yang Sama Dua Kali.
Key deduplikasi yang benar-benar bisa diandalkan
Karena eventId tidak bisa dipakai, bangun key komposit dari field yang bersama-sama mengidentifikasi satu perubahan:
CREATE TABLE processed_hubspot (
object_id BIGINT NOT NULL,
subscription_type TEXT NOT NULL,
occurred_at BIGINT NOT NULL,
PRIMARY KEY (object_id, subscription_type, occurred_at)
);
Satu objek, satu jenis perubahan, satu milidetik. Dua edit yang benar-benar berbeda pada field yang sama di milidetik yang sama akan bertabrakan, risiko yang nyata tetapi bisa diabaikan untuk edit CRM yang dilakukan manusia, dan risiko yang nyata untuk impor massal. Untuk langganan perubahan properti, tambahkan propertyName ke key: subscriptionType sama saja apa pun properti yang berubah, jadi tanpa field itu, dua properti berbeda yang berubah di milidetik yang sama juga bertabrakan.
Ini perlu dinyatakan terang-terangan karena kualitasnya di bawah apa yang diberikan Stripe. Dengan ID event unik yang diterbitkan provider, deduplikasi itu pasti tepat. Dengan key komposit, deduplikasi adalah heuristik yang sangat baik. Tulis pilihan itu di samping definisi tabel agar orang berikutnya tahu itu disengaja.
Impor massal adalah uji stresnya
Pelanggan yang mengimpor 50.000 kontak menghasilkan 50.000 notifikasi dalam waktu singkat, dikemas dalam array. Saat itulah semua hal di atas berhenti menjadi teori secara bersamaan: batch sedang paling besar, database Anda sedang paling sibuk, batas waktu 5 detik sedang paling ketat, dan setiap kegagalan menghasilkan percobaan ulang yang tiba berjam-jam kemudian, berselang-seling dengan edit yang sedang berlangsung.
Jika Anda hanya pernah menguji dengan event tunggal buatan tangan, Anda belum menguji kasus yang membuatnya rusak. Kirim batch berisi 100 notifikasi ke diri sendiri dengan stempel waktu yang sengaja diacak, lalu pastikan state akhirnya adalah yang memiliki occurredAt terbaru, bukan yang tiba terakhir.
Peran AnyHook
Tidak ada bagian dari masalah urutan ini yang terletak di lapisan transport. Jika HubSpot mengirim notifikasi kedua setelah notifikasi keempat, tidak ada relay yang bisa membatalkan pengiriman itu, dan produk apa pun yang mengeklaim sebaliknya sedang melebih-lebihkan.
Yang diubah relay adalah semua hal di sekitarnya.
Arahkan HubSpot ke in.anyhook.net/you/hubspot, dan AnyHook mengembalikan 200 dalam sekitar satu detik berapa pun ukuran batch-nya, sehingga batas waktu 5 detik tidak lagi membatasi cara Anda memproses. Pengiriman ke endpoint Anda dicoba ulang dengan jadwalnya sendiri, bukan jadwal HubSpot, sehingga gangguan singkat tidak memperlebar rentang waktu tertukarnya urutan hingga 24 jam.
Selain itu:
- Setiap batch disimpan bersama header dan body-nya sebelum pengiriman dicoba, sehingga pertanyaan “apa yang sebenarnya kami terima, dan dalam urutan apa” bisa dijawab dari log, bukan direkonstruksi dari state database Anda saat ini.
- Memutar ulang (replay) semua event yang gagal dalam satu rentang waktu memungkinkan Anda menjalankan ulang apa yang hilang dari sebuah impor dengan handler yang sudah diperbaiki, alih-alih meminta pelanggan mengimpor lagi.
attemptNumberdi atas 0 bukan lagi satu-satunya petunjuk bahwa ada yang salah, karena kegagalan pengiriman dihitung dan diperingatkan pada 1, 5, dan 20.
Handler Anda tetap butuh version guard. Bagian itu tanggung jawab Anda.
Kesimpulan
Kategori CRM menuntut refleks yang berbeda dari kategori pembayaran. Baca dokumentasi field sebelum Anda memasang indeks apa pun, karena HubSpot luar biasa jujur bahwa ID event-nya tidak akan sanggup menanggung beban yang ingin Anda letakkan di atasnya. Urutkan berdasarkan occurredAt, lakukan deduplikasi dengan key komposit yang Anda pilih secara sadar, dan lindungi setiap penulisan dari state usang.
Jika handler Anda saat ini menjalankan UPDATE ... WHERE id = $1 tanpa kondisi stempel waktu, pertanyaannya bukan apakah handler itu sudah menulis data usang. Pertanyaannya adalah apakah sudah ada yang menyadarinya.