Chatera Docs
API & Integrasi

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/webhooks

Scope 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"
}
FieldWajibKeterangan
nameLabel untuk identifikasi di dashboard
urlEndpoint HTTPS publik milik kamu (HTTP tidak didukung)
eventsArray event yang ingin diterima
secretString 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

EventKapan dikirim
message.inboundPesan baru masuk dari pelanggan
message.sentPesan dari kamu berhasil dikirim ke WhatsApp
message.deliveredPesan sampai di perangkat penerima
message.readPesan dibaca penerima (centang biru WhatsApp)
message.failedPengiriman gagal (alasan di payload)
contact.createdKontak baru dibuat (lewat API atau muncul dari pesan masuk)
contact.updatedField kontak diubah
conversation.openedPercakapan baru dimulai (kontak menghubungi setelah lama tidak aktif)
conversation.assignedPercakapan di-assign ke agen tertentu
conversation.resolvedPercakapan 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
HeaderKeterangan
X-Chatera-EventNama event (sama dengan event di body)
X-Chatera-TimestampUnix timestamp dalam milidetik
X-Chatera-Delivery-IdID unik delivery — pakai untuk deduplikasi
X-Chatera-SignatureHMAC-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 dataTipeKeterangan
messageIdUUIDID pesan di Chatera
whatsappMessageIdstringID dari Meta (wamid.xxx)
conversationIdUUIDID percakapan
contactIdUUIDID kontak pengirim
channelstringSaat ini selalu "whatsapp"
channelIdUUID | nullID saluran tempat pesan diterima
directionstring"inbound" atau "outbound"
contentTypestringtext | image | audio | video | file | interactive
statusstringdelivered, read, failed
timestampISO 8601Waktu event di server
content.textstringHanya untuk contentType: text. Kosong untuk media.
sender.phonestringNomor WhatsApp pengirim (tanpa +)
sender.namestringNama profil WhatsApp
sender.bsuidstringBusiness 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', 200

Praktik 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.

On this page