Webhooks

Live

Terima event booking Termilo di sistem Anda lewat webhook: nama event, payload, signing, verifikasi, retry, dan replay. Outbound delivery, riwayat percobaan, dan replay sudah aktif.

Lihat .md

Status: live#

Outbound webhook aktif

Subscription tenant, signing HMAC-SHA256, outbox, queue delivery, retry, riwayat percobaan, dan replay tersedia. Signing secret hanya ditampilkan sekali saat subscription dibuat.

Delivery bersifat at-least-once dan urutan antar-booking tidak dijamin. Penerima wajib memverifikasi signature lalu deduplikasi berdasarkan event ID sebelum menjalankan efek samping.

Nama event#

Setiap mutasi booking menghasilkan satu event dengan nama kanonik berformat subjek.aksi. Filter langganan webhook Anda berdasarkan nama-nama ini.

EventKapan dipicu
booking.createdBooking baru dibuat (status held).
booking.confirmedBooking dikonfirmasi tenant.
booking.rescheduledWaktu booking dipindahkan.
booking.cancelledBooking dibatalkan (held atau confirmed).
booking.completedBooking ditandai selesai.
booking.no_showPelanggan tidak hadir.
booking.expiredHold booking kedaluwarsa.

Amplop event#

Semua event keluar memakai payload versi 1 yang sama. Metadata ada di tingkat atas dan snapshot booking/service ada di data. Semua timestamp body memakai UTC epoch milidetik.

PARAMETERTIPEWAJIBKETERANGAN
idstringwajibEvent ID stabil. Simpan untuk deduplikasi.
typeenumwajibNama event kanonik, mis. booking.created.
payloadVersionintegerwajibVersi payload; saat ini 1.
occurredAtMsepoch mswajibWaktu event terjadi dalam UTC epoch milidetik.
data.bookingobjectwajibbookingId, tenantId, serviceId, status, waktu, dan timezone.
data.serviceobjectwajibserviceId, slug, dan nama service.
data.customerobjectwajibCustomer snapshot minimum untuk integrasi tenant.

Contoh amplop untuk booking.created (bentuk live):

POST · webhookapplication/json
{
"id": "evt_8f3a…",
"type": "booking.created",
"payloadVersion": 1,
"occurredAtMs": 1750732800000,
"data": {
"booking": {
"bookingId": "book_2c91…",
"tenantId": "tenant_demo",
"serviceId": "service_onboarding",
"status": "held",
"startsAtMs": 1750744800000,
"endsAtMs": 1750746600000,
"timezone": "Asia/Jakarta"
},
"service": {
"serviceId": "service_onboarding",
"slug": "onboarding-konektor",
"name": "Onboarding Konektor"
},
"customer": {
"customerId": "cus_…",
"name": "Customer",
"primaryEmail": "customer@example.com"
}
}
}

Pengiriman (outbox)#

Pengiriman memakai pola outbox. Setiap mutasi booking lebih dulu menulis satu baris outbox yang immutable di D1, lalu sebuah queue consumer mengirimkannya ke endpoint Anda. Setiap percobaan kirim tercatat di integration_delivery_attempts. Pola ini menjaga event tidak hilang meski pengiriman sempat gagal.

Urutan & at-least-once
Karena outbox memungkinkan kirim ulang, satu event bisa tiba lebih dari sekali. Perlakukan pengiriman sebagai at-least-once dan idempoten berdasarkan id.

Tanda tangan & verifikasi#

Setiap payload ditandatangani per tenant (HMAC). Setiap pengiriman membawa header berikut. Verifikasi tanda tangan sebelum mem-parse body.

HeaderKeterangan
X-Termilo-Signaturesha256=<hex> dari HMAC-SHA256 atas timestamp.body mentah.
X-Termilo-TimestampUTC epoch detik yang ikut ditandatangani.
X-Termilo-Event-IdEvent ID stabil untuk deduplikasi.
X-Termilo-Event-TypeNama event yang sama dengan type pada body.

Bandingkan X-Termilo-Signature dengan HMAC-SHA256 atas timestamp.body mentah memakai signing secret tenant dan perbandingan aman terhadap timing:

import { createHmac, timingSafeEqual } from "node:crypto"; export function verifyTermilo(rawBody, timestamp, signatureHeader, secret) { const digest = createHmac("sha256", secret) .update(timestamp + "." + rawBody) .digest("hex"); const a = Buffer.from("sha256=" + digest); const b = Buffer.from(signatureHeader ?? ""); return a.length === b.length && timingSafeEqual(a, b); }

Retry & replay#

Termilo hanya mengulang kirim untuk status/timeout yang retryable, memakai backoff eksponensial:

  • Maksimal 6 percobaan per delivery, dengan timeout 10 detik tiap percobaan.
  • Retry dimulai setelah 60 detik lalu memakai backoff eksponensial.
  • Dashboard dapat replay satu delivery yang tercatat; riwayat attempt tetap dipertahankan.
  • Outbox deduplikasi memakai event ID + subscription ID. Penerima tetap wajib menyimpan event ID karena delivery bersifat at-least-once.
Balas cepat dengan 2xx
Endpoint Anda harus membalas 2xx dalam 10 detik. Lakukan pekerjaan berat secara asinkron setelah mengakui penerimaan, agar tidak memicu retry yang tidak perlu.

Webhook masuk#

Tidak tersediaEndpoint berikut tidak tersedia.

Webhook masuk dari penyedia eksternal tidak tersedia. Nama endpoint di bawah menggambarkan bentuk yang dipakai bila fitur ini dibangun: setiap endpoint memverifikasi tanda tangan, menyimpan event sebelum memutasi data, baru meng-ack setelah event tersimpan secara durable, lalu menjalankan efek samping lewat queue.

Endpoint (tidak tersedia)Keterangan
POST /webhooks/payment/:providerNotifikasi pembayaran/settlement dari penyedia pembayaran.
POST /webhooks/integrations/:connectorKeyEvent masuk dari connector CRM/SaaS.
POST /webhooks/provider/:providerEvent dari penyedia kalender/video (mis. perubahan event).

Pertanyaan umum#

Sudah. Buat subscription dari dashboard, simpan signing secret yang ditampilkan sekali, lalu pilih event booking yang dibutuhkan.