Webhooks
Panduan menerima event outbound Dalekta: setup endpoint, header signature, HMAC verification, payload envelope, retry, dan idempotency.
Ringkasan
Webhook Dalekta mengirim event workspace ke endpoint HTTPS milik user saat lead atau chat berubah. Gunakan webhook ketika sistem eksternal perlu menerima push event tanpa polling Agent API.
Setup endpoint
- Buka dashboard Dalekta.
- Masuk ke menu
API & Webhook. - Klik
Tambah webhook. - Isi nama endpoint dan URL HTTPS receiver.
- Pilih event yang ingin dikirim.
- Copy signing secret
whsec_...saat endpoint berhasil dibuat. Secret hanya tampil satu kali. - Klik tombol test di daftar webhook untuk memastikan receiver membalas 2xx.
- Buka log delivery dari row webhook untuk melihat response, error, retry schedule, dan menjalankan retry manual untuk delivery
FAILED.
Contoh pemakaian webhook
Webhook dipakai saat sistem luar perlu langsung menerima notifikasi dari Dalekta. Bedanya dengan Agent API: Agent API cocok untuk pertanyaan seperti tolong cek berapa leads hari ini, sedangkan webhook cocok untuk trigger otomatis seperti kalau ada lead baru, kirim ke CRM.
| Kebutuhan user | Event webhook | Yang dilakukan receiver |
|---|---|---|
kalau ada lead baru, kirim ke CRM | lead.created | Receiver membuat atau memperbarui contact/deal di CRM memakai data.lead dan attribution. |
kalau status lead berubah, update pipeline CRM | lead.status_changed | Receiver mencari lead berdasarkan id atau kode, lalu memindahkan stage pipeline sesuai status baru. |
kalau closing, kirim notifikasi dan simpan revenue | lead.closed | Receiver mengirim notifikasi ke sistem kerja tim dan menyimpan closingValue jika ada. |
kalau ada chat baru, trigger AI follow up | chat.message.created | Receiver membuat job follow up. Jika perlu isi chat lengkap, agent membaca detail lewat Agent API messages endpoint dengan allowlist session. |
kalau data lead berubah, sync ke spreadsheet | lead.updated | Receiver melakukan upsert baris spreadsheet agar data lead di sistem luar tetap mengikuti Dalekta. |
Contoh flow: lead baru masuk
1. Customer klik WhatsApp dari landing page.
2. Lead tercatat di Dalekta.
3. Dalekta membuat event `lead.created`.
4. Dalekta POST payload ke endpoint webhook yang subscribe `lead.created`.
5. Receiver verify `X-Dalekta-Signature`.
6. Receiver simpan `payload.id` untuk idempotency.
7. Receiver upsert contact/deal di CRM.
8. Receiver balas HTTP 200 agar delivery dianggap sukses.Headers
Dalekta mengirim payload sebagai JSON dan menambahkan header berikut pada setiap delivery.
| Header | Isi |
|---|---|
Content-Type | application/json |
X-Dalekta-Signature | t=<unix_timestamp>,v1=<hex_hmac_sha256> |
X-Dalekta-Event | Nama event, misalnya lead.status_changed. |
X-Dalekta-Delivery-Id | ID delivery unik dari log Dalekta. |
User-Agent | Dalekta-Webhook/1.0 |
Content-Type: application/json
X-Dalekta-Signature: t=1785074400,v1=<hex_hmac_sha256>
X-Dalekta-Event: lead.status_changed
X-Dalekta-Delivery-Id: whd_xxx
User-Agent: Dalekta-Webhook/1.0Payload envelope
Semua event memakai envelope yang sama. Field id adalah event id untuk idempotency lintas retry, sedangkan X-Dalekta-Delivery-Id adalah id delivery attempt/log.
{
"id": "evt_xxx",
"type": "lead.status_changed",
"createdAt": "2026-07-26T14:00:00.000Z",
"workspace": {
"id": "workspace_id",
"name": "Workspace name"
},
"data": {
"lead": {
"id": "lead_id",
"status": "CLOSING"
}
}
}| Field | Makna |
|---|---|
id | Event id stabil untuk dedupe. Simpan field ini di receiver. |
type | Nama event yang juga dikirim di X-Dalekta-Event. |
createdAt | Waktu event dibuat dalam ISO 8601 UTC. |
workspace | Workspace sumber event. |
data | Payload spesifik event. |
Verify signature
Signature dihitung dari string <timestamp>.<raw_json_body> memakai HMAC SHA-256 dan signing secret endpoint. Receiver harus memakai raw body asli, bukan hasil JSON stringify ulang.
- Ambil header
X-Dalekta-Signature. - Parse nilai
tdanv1. - Tolak request jika timestamp terlalu jauh dari waktu server, misalnya lebih dari 5 menit.
- Hitung HMAC SHA-256 dari
<t>.<raw_body>dengan signing secret. - Bandingkan hasil hex dengan
v1memakai timing-safe comparison.
const crypto = require("crypto");
function verifyDalektaSignature(rawBody, signatureHeader, secret) {
const parts = Object.fromEntries(
String(signatureHeader || "")
.split(",")
.map((part) => part.trim().split("="))
.filter(([key, value]) => key && value)
);
const timestamp = Number(parts.t);
const signature = parts.v1;
if (!Number.isFinite(timestamp) || !signature) return false;
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - timestamp) > 300) return false;
const expected = crypto
.createHmac("sha256", secret)
.update(timestamp + "." + rawBody)
.digest("hex");
const actualBuffer = Buffer.from(signature, "hex");
const expectedBuffer = Buffer.from(expected, "hex");
return actualBuffer.length === expectedBuffer.length && crypto.timingSafeEqual(actualBuffer, expectedBuffer);
}Contoh receiver
Contoh Express di bawah memakai raw body supaya signature bisa diverifikasi dengan benar sebelum payload diproses.
const express = require("express");
const app = express();
app.post("/dalekta/webhook", express.raw({ type: "application/json" }), async (req, res) => {
const rawBody = req.body.toString("utf8");
const secret = process.env.DALEKTA_WEBHOOK_SECRET;
if (!verifyDalektaSignature(rawBody, req.header("X-Dalekta-Signature"), secret)) {
return res.status(401).json({ error: "invalid_signature" });
}
const event = JSON.parse(rawBody);
const deliveryId = req.header("X-Dalekta-Delivery-Id");
// Simpan event.id atau deliveryId sebagai idempotency key sebelum menjalankan side effect.
if (await alreadyProcessed(event.id)) {
return res.status(200).json({ ok: true, duplicate: true });
}
await markProcessed(event.id, deliveryId);
await handleDalektaEvent(event);
return res.status(200).json({ ok: true });
});Retry dan idempotency
| Kondisi | Status delivery |
|---|---|
| Response HTTP 2xx | SUCCESS |
| Timeout 5 detik | RETRYING atau FAILED jika percobaan habis |
| Response non-2xx | RETRYING atau FAILED jika percobaan habis |
| Endpoint nonaktif | SKIPPED |
Retry berjalan bertahap setelah delivery gagal. Karena modelnya at-least-once, receiver harus aman menerima event yang sama lebih dari sekali.
Delivery log bisa dibuka dari daftar webhook di menu API & Webhook. Log menampilkan event id, status, jumlah attempt, HTTP status, error/response body ringkas, dan jadwal retry berikutnya.
Delivery yang sudah FAILED bisa dicoba ulang manual dari log selama endpoint masih aktif. Manual retry memakai signature baru dan tetap mempertahankan konteks delivery untuk audit.
- Simpan
payload.idsebagai idempotency key utama. - Simpan juga
X-Dalekta-Delivery-Iduntuk audit delivery attempt. - Pastikan side effect tidak berjalan dua kali untuk
payload.idyang sama. - Tetap balas 200 untuk duplicate yang sudah pernah diproses.
Event awal
| Event | Trigger |
|---|---|
lead.created | Lead baru dibuat dari tracking, klik WhatsApp, atau flow WhatsApp. |
lead.updated | Data lead berubah tanpa harus menunggu perubahan status. |
lead.status_changed | Status lead berubah, misalnya dari Masuk WA ke MQL atau Prospek. |
lead.closed | Lead pertama kali masuk closing atau punya nominal closing. |
chat.message.created | Pesan WhatsApp baru tersimpan. Payload chat tetap ringan. |
{
"id": "evt_lead_status_changed_xxx",
"type": "lead.status_changed",
"createdAt": "2026-07-26T14:00:00.000Z",
"workspace": {
"id": "workspace_id",
"name": "Workspace name"
},
"data": {
"lead": {
"id": "lead_id",
"status": "CLOSING",
"previousStatus": "BOOKING",
"currency": "IDR",
"closingValue": 2500000,
"closingDate": "2026-07-26T14:00:00.000Z"
},
"attribution": {
"sourcePlatform": "meta",
"utmCampaign": "campaign_name",
"adId": "ad_id"
}
}
}{
"id": "evt_chat_message_created_xxx",
"type": "chat.message.created",
"createdAt": "2026-07-26T14:00:00.000Z",
"workspace": {
"id": "workspace_id",
"name": "Workspace name"
},
"data": {
"message": {
"id": "wa_message_id",
"leadId": "lead_id",
"waSessionId": "wa_session_id",
"direction": "inbound",
"type": "text",
"timestamp": "2026-07-26T14:00:00.000Z"
},
"customer": {
"phoneNumber": "628xxxxxxxxxx",
"name": "Customer name"
},
"content": {
"availableViaAgentApi": true
}
}
}chat.message.created membawa payload ringan. Untuk membaca isi chat lengkap, gunakan Agent API messages endpoint dengan scope agent.chats.read dan WhatsApp session allowlist.