Sign inGet Started

Webhook & event

Ketika sesuatu terjadi di workspace Anda (email terkirim, penerima bounce, pesan WhatsApp dibaca), Bird mengirim POST event JSON bertanda tangan ke setiap endpoint webhook yang berlangganan tipe event tersebut. Bird mengikuti spesifikasi Standard Webhooks untuk header, penandatanganan, dan struktur payload, jadi jika Anda sudah memverifikasi webhook dari platform Standard Webhooks lain, kode verifikasi yang sama berfungsi di sini tanpa perubahan.
Untuk gambaran umum tentang endpoint webhook dan pengiriman, lihat Apa itu webhook?.

Membuat endpoint

Daftarkan endpoint di dashboard pada Developers > Webhooks, atau dari terminal dengan bird CLI:
Contoh kode
bird webhooks create https://example.com/webhooks/bird \
  --events email.delivered,email.bounced,email.complained \
  --description "Production delivery + bounce notifications"
Pengelolaan endpoint memerlukan scope webhooks. Sesi dashboard dan login CLI membawanya melalui peran pengguna Anda, dan kunci API juga dapat memilikinya: berikan webhooks:read untuk memeriksa endpoint dan percobaan pengiriman, atau webhooks:write untuk mengelolanya. Operasi dasarnya dimulai di POST /v1/webhooks.
Halaman Webhooks di dashboard Bird, menampilkan endpoint aktif beserta event yang dilanggannya
URL endpoint harus HTTPS, maksimal 2048 karakter, dan dapat dijangkau publik. URL pada alamat privat, loopback, link-local, atau alamat internal lainnya ditolak dengan 422 saat Anda membuat atau memperbarui endpoint. Pengiriman berasal dari infrastruktur pengiriman Bird di luar jaringan Anda.
Array events mencantumkan hingga 100 tipe dari katalog event. Endpoint hanya menerima tipe yang dicantumkannya. Gunakan PATCH /v1/webhooks/{webhook_id} untuk mengganti seluruh daftar bagi pengiriman mendatang. Untuk menerima setiap event, langganan setiap tipe: tipe di luar katalog ditolak dengan 422, termasuk wildcard seperti sms.*. Langganan yang sudah ada tidak bertambah otomatis saat tipe baru tersedia.
Respons pembuatan menyertakan secret penandatanganan endpoint (berawalan whsec_) tepat satu kali. Simpan segera di secret manager Anda; nilai ini tidak dapat diambil lagi, dan jika Anda kehilangannya, rotasi.
Contoh kode
{
  "id": "whk_01ky7q639cfh99xb7ysy2x2gzj",
  "url": "https://example.com/webhooks/bird",
  "events": ["email.delivered", "email.bounced", "email.complained"],
  "description": "Production delivery + bounce notifications",
  "status": "active",
  "secret": "whsec_c+dgRBsFELJ9mR4tu2cyhK0gTMMkqntv",
  "created_at": "2026-07-23T14:48:29.740Z",
  "updated_at": "2026-07-23T14:48:29.740Z"
}
Endpoint mendukung CRUD penuh: list, get, update, dan delete. Menghapus endpoint menghentikan semua pengiriman ke sana, termasuk percobaan ulang pengiriman gagal sebelumnya, dan tidak dapat dibatalkan; untuk menghentikan pengiriman sementara, atur status ke paused. Satu workspace dapat mendaftarkan beberapa endpoint, masing-masing dengan URL, filter event, dan secret sendiri.

Verifikasi tanda tangan

Setiap pengiriman membawa tiga header:
HeaderNilai
webhook-idMengidentifikasi pengiriman event. Percobaan ulang dan replay menggunakan nilai yang sama.
webhook-timestampUnix timestamp (detik) dari percobaan pengiriman ini
webhook-signaturev1,<base64 HMAC-SHA256>, mungkin beberapa tanda tangan dipisahkan spasi
Tanda tangan adalah HMAC-SHA256 atas string {webhook-id}.{webhook-timestamp}.{raw request body}, dikunci dengan secret endpoint Anda (hapus awalan whsec_ dan base64-decode sisanya untuk mendapatkan byte kunci). Handler Anda harus memverifikasi tanda tangan, menolak pengiriman yang webhook-timestamp-nya lebih dari 5 menit, dan mendeduplikasi berdasarkan webhook-id: Bird mengirim at-least-once, sehingga pengiriman yang sama bisa tiba lebih dari sekali.
Dengan Bird SDK, pemeriksaan tanda tangan dan timestamp cukup satu panggilan; deduplikasi tetap di handler Anda:
// Pass the RAW request body; set the secret via new BirdClient({ webhooks: { secret } }).
const event = bird.webhooks.unwrap(rawBody, headers);
console.log(event.type); // discriminated union: narrow on event.type
Menolak pengiriman dengan 400, seperti contoh di atas, tidak membuang event: kami mencoba ulang sesuai jadwal di bawah. Ini disengaja, dan memang yang Anda inginkan. Penyebab umum verifikasi gagal adalah secret yang belum dimiliki handler Anda, saat rotasi atau deploy yang salah, sehingga jendela percobaan ulang adalah kesempatan Anda memperbaiki secret dan tetap menerima event. Kembalikan 2xx hanya jika Anda benar-benar ingin membuang pengiriman tersebut.
Pustaka referensi Standard Webhooks mana pun juga bisa digunakan. Jika Anda memverifikasi secara manual, langkahnya adalah:
Contoh kode
import { createHmac, timingSafeEqual } from "node:crypto";

function verify(rawBody: string, headers: Record<string, string>, secret: string): boolean {
  const id = headers["webhook-id"];
  const timestamp = headers["webhook-timestamp"];
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false; // 5-minute tolerance

  const key = Buffer.from(secret.slice("whsec_".length), "base64");
  const expected = createHmac("sha256", key)
    .update(`${id}.${timestamp}.${rawBody}`)
    .digest("base64");

  // During secret rotation, the header can contain several signatures. Accept any match.
  return headers["webhook-signature"].split(" ").some((part) => {
    const sig = Buffer.from(part.replace(/^v1,/, ""), "base64");
    return (
      sig.length === Buffer.byteLength(expected, "base64") &&
      timingSafeEqual(sig, Buffer.from(expected, "base64"))
    );
  });
}
Selalu hitung HMAC atas byte mentah body request. Mem-parse dan men-serialize ulang JSON mengubah whitespace atau urutan key dan merusak tanda tangan.

Semantik pengiriman

Setiap pengiriman adalah satu event per POST HTTP dengan Content-Type: application/json, tanpa batching. Endpoint Anda punya 15 detik untuk merespons; status 2xx apa pun dianggap sukses, dan selainnya (termasuk redirect 3xx dan timeout) dianggap gagal. Setiap kegagalan mengikuti jadwal percobaan ulang yang sama. Status yang Anda kembalikan mengubah apa yang terlihat di log percobaan pengiriman, bukan apakah kami mencoba ulang: tidak ada kode status yang menghentikan pengiriman lebih awal. Respons cepat dan proses secara asinkron: antrikan event dan kembalikan 200 sebelum melakukan pekerjaan sebenarnya.
Setelah percobaan pertama, pengiriman gagal dicoba ulang sesuai jadwal ini, dengan jitter ±20% agar percobaan ulang tidak tersinkronisasi:
Percobaan ulangJeda setelah percobaan sebelumnya
15 detik
25 menit
330 menit
42 jam
55 jam
610 jam
710 jam
Totalnya delapan percobaan dalam kurang lebih 27,5 jam. 429 atau timeout menaikkan jeda terjadwal yang lebih pendek dari 60 detik menjadi 60 detik, yang dalam praktiknya hanya memengaruhi percobaan ulang pertama: setelah jitter, percobaan tiba 48 hingga 72 detik kemudian. Header Retry-After pada respons gagal dapat memperpanjang waktu tunggu berikutnya. Kami menerima header tersebut sebagai delay-seconds atau tanggal HTTP. Jeda yang diminta lebih lama dari jeda terjadwal akan menggantikannya, dibatasi maksimal dua kali jeda terjadwal (setelah kenaikan 60 detik); yang lebih pendek diabaikan, sehingga header tidak pernah memajukan percobaan ulang. Jitter diterapkan di atasnya. Setiap percobaan ulang membawa webhook-id yang sama, dan inilah yang membuat deduplikasi berfungsi. Setelah percobaan ulang terakhir, pengiriman gagal secara permanen; replay memulihkannya.
Pengiriman tidak berurutan. email.delivered bisa tiba sebelum email.accepted untuk pesan yang sama, terutama ketika percobaan ulang terlibat. Urutkan berdasarkan field timestamp di dalam payload event, bukan berdasarkan urutan kedatangan.

Mengelola endpoint Anda

Pengiriman uji coba

POST /v1/webhooks/{webhook_id}/test mengirim event sintetis bertanda tangan ke endpoint Anda dan mengembalikan hasilnya secara sinkron: apakah endpoint Anda menerimanya, status HTTP yang dikembalikan, dan latensi round-trip. Body uji coba adalah stub JSON minimal yang hanya membawa type event, ditandatangani persis seperti pengiriman nyata; isinya tidak mencerminkan payload event sesungguhnya. Kirim {"event_type": "email.delivered"} untuk memilih tipe apa pun dari katalog, baik dilanggan maupun tidak, atau kosongkan body untuk menggunakan tipe event pertama yang dilanggan endpoint.
Endpoint Anda punya 10 detik untuk merespons. Endpoint yang tidak terjangkau menghasilkan status: failed di body respons, sementara request itu sendiri berhasil. Gunakan hasil ini untuk men-debug konektivitas. Pengiriman uji coba langsung menuju endpoint Anda: berfungsi pada endpoint yang dijeda dan tidak dicatat di log percobaan pengiriman. 412 berarti endpoint belum bisa diuji karena belum memiliki signing secret atau tipe event yang dilanggan.
Untuk pengujian end-to-end dengan alur event nyata, kirim ke alamat sandbox: pengiriman sandbox menghasilkan event webhook nyata melalui jalur pengiriman normal, cara terbaik untuk menguji handler Anda sebelum digunakan di produksi.

Memutar ulang pengiriman gagal

POST /v1/webhooks/{webhook_id}/replay mengantrikan pengiriman ulang untuk pengiriman yang gagal. Event yang sudah berhasil diterima endpoint dilewati, sehingga replay tidak pernah mengirim ganda; event yang dikirim ulang membawa webhook-id aslinya, sehingga pemeriksaan deduplikasi Anda mencakup replay juga. Hanya percobaan gagal yang diputar ulang: event yang tidak pernah dikirim ke endpoint Anda tidak memiliki percobaan gagal, sehingga replay tidak memulihkannya.
Berikan timestamp since/until untuk membatasi jendela waktu (default: 24 jam terakhir hingga waktu permintaan). Kedua batas bersifat inklusif, dan keduanya memilih berdasarkan waktu pengiriman dicoba, bukan waktu event terjadi, sehingga percobaan ulang yang tertinggal satu hari dari event-nya masuk ke jendela berdasarkan jam percobaannya. Replay membaca log percobaan pengiriman, yang menyimpan data selama tiga hari, jadi itu adalah riwayat tertua yang dapat dijangkau: since yang lebih awal memperlebar jendela tanpa memulihkan data yang lebih lama. Satu replay mencakup maksimal 10.000 event tertua dalam jendela tersebut.
Request mengembalikan 202 dan event dikirim ulang secara asinkron. Pengiriman ulang mendapat satu percobaan, bukan jadwal percobaan ulang di atas. Percobaan dicatat dan pekerjaan selesai terlepas dari apakah endpoint Anda menerimanya, sehingga replay ke endpoint yang masih rusak hanya memakan satu request per event, bukan delapan; perbaiki endpoint dan replay lagi. Kegagalan tersebut tidak memengaruhi kesehatan endpoint: replay tidak dapat mendorong endpoint ke degraded atau menjeda otomatisnya. Pengiriman ulang yang diterima endpoint Anda menghapus keduanya.
Replay endpoint paused dan request tetap mengembalikan 202, tetapi tidak ada yang dikirim ulang. Aktifkan kembali terlebih dahulu, seperti dijelaskan di Jeda otomatis dan pengaktifan ulang.
Replay dibatasi 20 per organisasi per hari UTC; melebihi itu, request mengembalikan 429 (WebhookReplayQuotaExceeded). Respons tidak menyertakan jumlah atau task ID. Lacak hasilnya dengan GET /v1/webhooks/{webhook_id}/attempts, yang mencantumkan percobaan pengiriman terbaru dari terbaru ke terlama beserta kode status dan latensi. Setiap request HTTP memiliki entri sendiri, sehingga event yang dicoba ulang muncul sekali per percobaan, dan pengiriman ulang muncul sebagai satu entri tambahan.

Merotasi signing secret

POST /v1/webhooks/{webhook_id}/rotate-secret menghasilkan secret baru dan mengembalikannya satu kali. Selama 24 jam berikutnya, Bird menandatangani setiap pengiriman dengan kedua secret. Header webhook-signature berisi tanda tangan yang dipisahkan spasi (v1,<old> v1,<new>), memungkinkan Anda men-deploy secret baru selama masa tumpang tindih. Pustaka Standard Webhooks mencoba semua tanda tangan secara otomatis. Setelah 24 jam, secret lama berhenti menandatangani. Endpoint menampung maksimal 5 secret valid secara bersamaan, sehingga merotasi berulang kali dalam jendela tumpang tindih gagal dengan WebhookTooManySecrets sampai secret yang lebih lama kedaluwarsa.

Jeda otomatis dan pengaktifan ulang

status endpoint adalah active, degraded, atau paused. Kegagalan pengiriman baru-baru ini menandai endpoint degraded sebagai peringatan kesehatan; kami tetap mengirim dan mencoba ulang. Endpoint yang gagal terus-menerus selama sekitar lima hari otomatis paused dan semua pengiriman berhenti; satu pengiriman berhasil selama periode itu mengatur ulang hitungan. Endpoint yang dijeda tidak pernah melanjutkan sendiri. Aktifkan kembali dengan PATCH /v1/webhooks/{webhook_id} dan {"status": "active"} (atau dari halaman Webhooks di dashboard), lalu replay untuk mengirim ulang percobaan yang gagal sebelum dijeda. Aktifkan kembali terlebih dahulu: replay yang diminta saat endpoint masih dijeda tidak mengirim ulang apa pun. Event yang tiba saat endpoint dijeda tidak pernah dikirim, sehingga replay tidak memulihkannya.
Salah satu hal berikut mengembalikan endpoint degraded ke active:
Yang menghapusnyaAlasan
Pengiriman berhasilEndpoint kembali menerima event.
Mengubah url endpointKegagalan yang tercatat menggambarkan tujuan yang tidak lagi Anda gunakan.
Mengaktifkan kembali endpoint pausedEndpoint kembali beroperasi, sehingga kegagalan lamanya tidak berlaku lagi.
Pengiriman uji coba yang mengembalikan 2xxAnda telah menunjukkan bahwa endpoint dapat dijangkau.
Mengedit deskripsi endpoint atau tipe event yang dilanggannya tidak menunjukkan apa pun tentang keterjangkauan, sehingga degraded tetap di tempatnya, begitu pula pengiriman uji coba yang gagal.
Kami mengirim email ke pemilik organisasi saat sebuah endpoint pertama kali menjadi degraded, sekali per episode, bukan sekali per pengiriman gagal. Degradasi berikutnya setelah pemulihan akan mengirim email lagi, dengan cooldown 24 jam: kami mengirim maksimal satu email degradasi per endpoint setiap 24 jam, sehingga endpoint yang berganti-ganti antara active dan degraded tidak membanjiri kotak masuk mereka. Mengubah url endpoint mereset cooldown, sehingga degradasi pertama di URL baru dapat mengirim email meskipun kurang dari 24 jam sejak email terakhir.

Katalog event

Payload event berisi fakta ringkas yang dicakupkan ke penerima untuk korelasi dengan sistem Anda. Payload tidak berisi resource lengkap. Jika Anda memerlukan konteks lebih, ambil resource berdasarkan ID-nya. Tipe event mengikuti penamaan resource.action dan dikelompokkan per produk; halaman event setiap produk mencantumkan field payload per event:
  • Event email: siklus hidup pengiriman (email.accepted hingga email.delivered atau email.bounced), interaksi (email.opened, email.clicked), berhenti berlangganan, dan email masuk
  • Event SMS: siklus hidup pesan dari sms.accepted ke status terminal
  • Webhook WhatsApp: whatsapp.accepted hingga whatsapp.delivered, whatsapp.read, whatsapp.failed, whatsapp.rejected, whatsapp.received untuk pesan masuk, whatsapp.reacted saat pengguna memberikan reaksi pada pesan Anda, serta whatsapp.group.join_request_created dan whatsapp.group.join_request_revoked saat seseorang meminta bergabung dengan grup yang memerlukan persetujuan atau menarik permintaan tersebut
  • Event Verify: siklus hidup verifikasi (verify.verification.created, verify.verification.verified) dan pengiriman setiap percobaan kode verifikasi (verify.attempt.sent, verify.attempt.delivered, verify.attempt.undelivered)
  • Event Preference: catatan persetujuan lintas kanal: preference.granted, preference.revoked, dan preference.deleted
Setiap body pengiriman adalah envelope bersarang Standard Webhooks dengan type, timestamp, dan objek data yang spesifik per tipe. Header webhook-id membawa identitas event. timestamp di envelope mencatat kapan event terjadi. Header webhook-timestamp mencatat percobaan pengiriman saat ini dan berubah setiap percobaan ulang.
Contoh kode
{
  "type": "email.delivered",
  "timestamp": "2026-06-10T14:30:00Z",
  "data": {
    "email_id": "em_01krdgeqcxet5s7t44vh8rt9mg",
    "recipient_id": "er_01krdgeqcxet5s7t44vh8rt9mg",
    "workspace_id": "ws_01krdgeqcxet5s7t44vh8rt9mg",
    "recipient": "user@example.com",
    "recipient_role": "to",
    "tags": [{ "name": "category", "value": "welcome" }],
    "metadata": { "order_id": "ord_123" },
    "broadcast_id": null
  }
}
data setiap event email menyertakan email_id, recipient_id, workspace_id, alamat recipient, dan recipient_role envelope-nya. Juga menyertakan tags dan metadata dari request pengiriman, atau null jika tidak disediakan. Juga membawa broadcast_id, yang menyebutkan broadcast tempat pengiriman tersebut termasuk, atau null jika tidak ada broadcast di baliknya. Pada email.unsubscribed dan email.list_unsubscribed, null tidak meniadakan kemungkinan broadcast; event email menjelaskan alasannya. Tipe event menambahkan field-nya sendiri ke basis ini. Setiap varian memiliki set field yang stabil: field bersifat required secara default, dan kehadirannya hanya bergantung pada tipe event.
Nama event tidak pernah diganti, dan tipe baru ditambahkan seiring produk diluncurkan, jadi buat handler Anda mengabaikan tipe yang tidak dikenalinya.

Event preference

Preferensi yang dinyatakan (pemberian persetujuan dan pencabutan yang dijelaskan dalam panduan setiap kanal: email, SMS, WhatsApp) mencakup lintas kanal, sehingga event-nya mencantumkan kanal di dalam payload, bukan di dalam tipe. preference.granted terpicu saat pemberian persetujuan berlaku, preference.revoked saat pencabutan berlaku, dan preference.deleted saat pernyataan yang tercatat dihapus dan kuncinya kembali tidak memiliki catatan. Sebuah event berarti catatan terkini kunci tersebut berubah: pernyataan yang mengulangi catatan terkini tidak memicu apa pun, dan pernyataan yang ditolak karena tidak berurutan juga tidak memicu apa pun. timestamp pada envelope adalah waktu pernyataan tersebut berlaku, yang untuk pernyataan bertanggal mundur adalah waktu pernyataan dibuat, bukan waktu pernyataan diterima oleh Bird.
Setiap payload membawa preference key lengkap: channel, handle, sender_scope, dan topic_id, dengan field pembatas bernilai present-with-null jika tidak mempersempitnya. Di samping key terdapat coverage pernyataan, preference_id, transition_id dari entri riwayat yang ditambahkan oleh penulisan, dan contact_id yang handle-nya cocok saat pernyataan dicatat, atau null:
Contoh kode
{
  "type": "preference.revoked",
  "timestamp": "2026-08-25T14:52:03.192524705Z",
  "data": {
    "preference_id": "prf_01krdgeqcxet5s7t44vh8rt9mg",
    "transition_id": "prt_01krdgeqcxet5s7t44vh8rt9mg",
    "channel": "sms",
    "handle": "+15550001234",
    "sender_scope": null,
    "topic_id": null,
    "coverage": "non_transactional",
    "contact_id": null
  }
}

Langkah selanjutnya