Webhook
Terima notifikasi otomatis ketika ada pesan masuk, status berubah, atau percakapan baru di Chatera.
Webhook memungkinkan Chatera mengirim event ke server kamu secara real-time. Pakai webhook untuk:
- Memicu otomatisasi internal saat ada pesan masuk.
- Menyinkronkan status pesan ke database CRM kamu.
- Membuat notifikasi internal (Slack, email, dashboard) saat ada lead baru.
Membuat webhook
POST /v1/webhooksScope yang dibutuhkan: webhooks:manage
Request body
{
"name": "Notifikasi Pesan Masuk",
"url": "https://aplikasi-anda.com/webhook/chatera",
"events": [
"message.inbound",
"message.delivered",
"message.read"
],
"secret": "rahasia_panjang_dan_acak_minimal_32_karakter"
}| Field | Wajib | Keterangan |
|---|---|---|
name | ✅ | Label untuk identifikasi di dashboard |
url | ✅ | Endpoint HTTPS publik milik kamu (HTTP tidak didukung) |
events | ✅ | Array event yang ingin diterima |
secret | — | String rahasia untuk verifikasi signature (sangat dianjurkan) |
Response sukses
{
"success": true,
"data": {
"id": "wbk_9b8c1234e5f64789",
"name": "Notifikasi Pesan Masuk",
"url": "https://aplikasi-anda.com/webhook/chatera",
"events": ["message.inbound", "message.delivered", "message.read"],
"status": "active",
"created_at": "2026-04-27T11:00:00Z"
}
}secret tidak dikembalikan di response. Simpan secret di sisi
kamu sendiri saat pertama kali create.
Edit secret di dashboard
Saat membuka form edit webhook di dashboard, kolom Secret Key akan tampil kosong — ini normal. Biarkan kosong untuk mempertahankan secret yang sudah tersimpan, atau isi nilai baru untuk menggantinya.
Daftar event
| Event | Kapan dikirim |
|---|---|
message.inbound | Pesan baru masuk dari pelanggan |
message.sent | Pesan dari kamu berhasil dikirim ke WhatsApp |
message.delivered | Pesan sampai di perangkat penerima |
message.read | Pesan dibaca penerima (centang biru WhatsApp) |
message.failed | Pengiriman gagal (alasan di payload) |
contact.created | Kontak baru dibuat (lewat API atau muncul dari pesan masuk) |
contact.updated | Field kontak diubah |
conversation.opened | Percakapan baru dimulai (kontak menghubungi setelah lama tidak aktif) |
conversation.assigned | Percakapan di-assign ke agen tertentu |
conversation.resolved | Percakapan ditandai selesai |
Headers yang dikirim Chatera
Setiap delivery webhook memiliki header berikut:
X-Chatera-Event: message.inbound
X-Chatera-Timestamp: 1745749893228
X-Chatera-Delivery-Id: wh_1745749893228_abc123xyz
X-Chatera-Signature: sha256=a3f2c...
Content-Type: application/json| Header | Keterangan |
|---|---|
X-Chatera-Event | Nama event (sama dengan event di body) |
X-Chatera-Timestamp | Unix timestamp dalam milidetik |
X-Chatera-Delivery-Id | ID unik delivery — pakai untuk deduplikasi |
X-Chatera-Signature | HMAC-SHA256 signature — lihat di bawah |
Format payload
Semua payload memakai struktur top-level yang sama. Field data
berbeda per event.
{
"id": "wh_1745749893228_abc123xyz",
"event": "message.inbound",
"timestamp": "2026-04-27T11:00:00.000Z",
"organization_id": "9b8c1234-e5f6-4789-90ab-cdef01234567",
"data": { /* berbeda per event */ }
}Payload message.inbound
{
"id": "wh_1745749893228_abc123xyz",
"event": "message.inbound",
"timestamp": "2026-04-27T11:00:00.000Z",
"organization_id": "9b8c1234-e5f6-4789-90ab-cdef01234567",
"data": {
"messageId": "12345678-90ab-cdef-1234-567890abcdef",
"whatsappMessageId": "wamid.HBgNNjI4MTIzNDU2Nzg5...",
"conversationId": "00112233-4455-6677-8899-aabbccddeeff",
"contactId": "ffeedd99-8877-6655-4433-221100ffeedd",
"channel": "whatsapp",
"channelId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"direction": "inbound",
"contentType": "text",
"status": "delivered",
"timestamp": "2026-04-27T11:00:00.000Z",
"content": {
"text": "Halo, saya ingin bertanya tentang produk"
},
"sender": {
"phone": "628123456789",
"name": "Yudha Pranata",
"bsuid": "123456789987654321"
}
}
}Field di data | Tipe | Keterangan |
|---|---|---|
messageId | UUID | ID pesan di Chatera |
whatsappMessageId | string | ID dari Meta (wamid.xxx) |
conversationId | UUID | ID percakapan |
contactId | UUID | ID kontak pengirim |
channel | string | Saat ini selalu "whatsapp" |
channelId | UUID | null | ID saluran tempat pesan diterima |
direction | string | "inbound" atau "outbound" |
contentType | string | text | image | audio | video | file | interactive |
status | string | delivered, read, failed |
timestamp | ISO 8601 | Waktu event di server |
content.text | string | Hanya untuk contentType: text. Kosong untuk media. |
sender.phone | string | Nomor WhatsApp pengirim (tanpa +) |
sender.name | string | Nama profil WhatsApp |
sender.bsuid | string | Business Suite User ID |
Payload contact.created
{
"id": "wh_1745749893228_abc123xyz",
"event": "contact.created",
"timestamp": "2026-04-27T11:00:00.000Z",
"organization_id": "9b8c1234-e5f6-4789-90ab-cdef01234567",
"data": {
"contactId": "ffeedd99-8877-6655-4433-221100ffeedd",
"phone": "+628123456789",
"name": "Yudha Pranata",
"email": null,
"channelId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"timestamp": "2026-04-27T11:00:00.000Z"
}
}Payload conversation.opened
{
"id": "wh_1745749893228_abc123xyz",
"event": "conversation.opened",
"timestamp": "2026-04-27T11:00:00.000Z",
"organization_id": "9b8c1234-e5f6-4789-90ab-cdef01234567",
"data": {
"conversationId": "00112233-4455-6677-8899-aabbccddeeff",
"contactId": "ffeedd99-8877-6655-4433-221100ffeedd",
"channelId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"status": "open",
"timestamp": "2026-04-27T11:00:00.000Z"
}
}Payload conversation.assigned
{
"id": "wh_1745749893228_abc123xyz",
"event": "conversation.assigned",
"timestamp": "2026-04-27T11:00:00.000Z",
"organization_id": "9b8c1234-e5f6-4789-90ab-cdef01234567",
"data": {
"conversationId": "00112233-4455-6677-8899-aabbccddeeff",
"contactId": "ffeedd99-8877-6655-4433-221100ffeedd",
"assigneeId": "agent-uuid-1234",
"channelId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"timestamp": "2026-04-27T11:00:00.000Z"
}
}Verifikasi signature
Chatera menandatangani setiap webhook delivery menggunakan HMAC-SHA256 dengan format Stripe-style:
signature = "sha256=" + HMAC-SHA256(`${timestamp}.${rawBody}`, secret)timestamp adalah nilai dari header X-Chatera-Timestamp, dan
rawBody adalah body request mentah (sebelum di-parse JSON).
Wajib pakai raw body
Hitung signature sebelum body di-parse JSON. Banyak framework
web (Express dengan express.json()) otomatis parse body —
ini akan gagal verifikasi signature. Gunakan express.raw()
atau equivalent untuk endpoint webhook.
Mengapa pakai timestamp?
Format ini meniru Stripe (bukan Meta yang hanya HMAC(rawBody, secret)).
Dengan menyertakan timestamp, signature mencegah replay attack —
attacker yang mencuri payload valid tidak bisa mengirim ulang karena
timestamp sudah kedaluwarsa.
Kami menyarankan untuk menolak request dengan timestamp lebih dari 5 menit dari waktu server kamu.
Contoh verifikasi (Node.js + Express)
const crypto = require('crypto');
const express = require('express');
const app = express();
const SECRET = process.env.CHATERA_WEBHOOK_SECRET;
app.post(
'/webhook/chatera',
express.raw({ type: 'application/json' }),
(req, res) => {
const signature = req.headers['x-chatera-signature'];
const timestamp = req.headers['x-chatera-timestamp'];
const rawBody = req.body.toString('utf8');
// 1. Cek timestamp tidak terlalu lama (anti replay)
const age = Date.now() - Number(timestamp);
if (age > 5 * 60 * 1000) {
return res.status(401).send('Request expired');
}
// 2. Hitung expected signature
const expected = 'sha256=' + crypto
.createHmac('sha256', SECRET)
.update(`${timestamp}.${rawBody}`)
.digest('hex');
// 3. Bandingkan dengan timing-safe compare
const ok = signature.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
if (!ok) {
return res.status(401).send('Invalid signature');
}
// 4. Aman untuk parse JSON & process
const event = JSON.parse(rawBody);
console.log('Received', event.event, event.id);
// ALWAYS respond 2xx FAST, do heavy work async
res.status(200).send('OK');
}
);
app.listen(3000);Contoh verifikasi (Python + Flask)
import hmac
import hashlib
import time
from flask import Flask, request, abort
app = Flask(__name__)
SECRET = b'rahasia_panjang_dan_acak_minimal_32_karakter'
@app.route('/webhook/chatera', methods=['POST'])
def webhook():
signature = request.headers.get('X-Chatera-Signature', '')
timestamp = request.headers.get('X-Chatera-Timestamp', '0')
raw_body = request.get_data(as_text=True)
# 1. Cek timestamp tidak terlalu lama
age_ms = time.time() * 1000 - int(timestamp)
if age_ms > 5 * 60 * 1000:
abort(401, 'Request expired')
# 2. Hitung expected signature
message = f'{timestamp}.{raw_body}'.encode('utf-8')
expected = 'sha256=' + hmac.new(SECRET, message, hashlib.sha256).hexdigest()
# 3. Timing-safe compare
if not hmac.compare_digest(signature, expected):
abort(401, 'Invalid signature')
# 4. Aman untuk process
event = request.get_json()
print('Received', event['event'], event['id'])
return 'OK', 200Praktik terbaik
Idempotensi
Chatera bisa mengirim event yang sama lebih dari satu kali kalau
endpoint kamu lambat / sempat down. Dedup via X-Chatera-Delivery-Id
— simpan di Redis dengan TTL 24 jam, dan abaikan request dengan
ID yang sudah pernah diproses.
- Respon 2xx secepat mungkin (idealnya
<3 detik). Lakukan pekerjaan berat (DB write, kirim notifikasi internal) di background worker. - Kalau response kamu non-2xx atau timeout, Chatera akan retry dengan exponential backoff hingga ~24 jam.
- Monitor delivery yang gagal di dashboard webhook — kalau pernah ada sequence delivery yang fail, ada kemungkinan kehilangan event.
- Verifikasi signature selalu — webhook publik adalah surface attack favorit. Jangan skip verifikasi "sementara" di production.