Menerima & Memverifikasi Webhook
Daftar event, bentuk kiriman, verifikasi tanda tangan (Node, PHP, Python), pengiriman ulang, dan uji kirim.
± 5 menit
Webhook mengirim kejadian ke server Anda begitu terjadi — tanpa perlu bertanya berulang-ulang ke API.
Daftar event#
message.received | Pesan masuk dari pelanggan (semua jenis, dengan tautan media sementara). |
|---|---|
message.status | Status pesan keluar berubah: terkirim, diterima, dibaca, atau gagal. |
message.sent | Pesan keluar dari mana pun: CS, AI Agent, automasi, broadcast, atau API. |
conversation.assigned | Percakapan ditugaskan ke anggota tim atau dilepas. |
conversation.closed | Percakapan ditandai selesai. |
ai.handover | AI Agent menyerahkan percakapan ke CS manusia. |
channel.connected | Nomor WhatsApp tersambung. |
channel.disconnected | Nomor WhatsApp terputus. |
broadcast.finished | Broadcast selesai, dihentikan, atau gagal — dengan ringkasan angka. |
contact.created | Kontak WhatsApp baru tersimpan (chat masuk, impor, Inbox, atau API). |
contact.tagged | Tag baru ditambahkan ke kontak WhatsApp (berisi tag yang ditambahkan). |
order.created | Pesanan baru dibuat (Inbox, AI Agent, tagihan, atau checkout halaman toko). |
order.paid | Pesanan lunas (Mayar, transfer dikonfirmasi, atau mutasi terdeteksi otomatis). |
order.shipped | Pesanan dikirim — kurir & resi diisi. |
ticket.created | Tiket baru dibuat oleh tim atau AI Agent. |
csat.submitted | Pelanggan memberi nilai survei kepuasan (CSAT). |
Bentuk kiriman#
POST JSON dengan header X-Marsisten-Event, X-Marsisten-Delivery, dan X-Marsisten-Signature (t=<detik>,v1=<hex>).
message.received
{
"id": "evt_0123456789abcdef",
"type": "message.received",
"created_at": "2026-10-30T02:12:00.000Z",
"profile_id": "pr_0123456789abcdef",
"livemode": true,
"data": {
"channel": {
"id": "chd_pr_0123456789abcdef",
"type": "wa_device",
"number": "6281234567890"
},
"contact": {
"id": "ct_0123456789abcdef",
"name": "Dina",
"phone": "6281311112222",
"tags": [
"VIP"
]
},
"conversation_id": "ct_0123456789abcdef",
"message": {
"id": "msg_0123456789abcdef",
"conversation_id": "ct_0123456789abcdef",
"direction": "inbound",
"type": "image",
"text": "Yang ini ready kak?",
"origin": "audience",
"status": "received",
"error": null,
"at": "2026-10-30T02:12:00.000Z",
"media": [
{
"id": "mm_msg_0123456789abcdef_0",
"type": "image",
"mime": "image/jpeg",
"filename": null,
"size": null,
"url": "https://api.contoh/v1/media/mm_…/content?exp=…&sig=…",
"expires_at": "2026-10-30T03:12:00.000Z"
}
]
}
}
}Verifikasi tanda tangan#
Hitung HMAC-SHA256 dari t + "." + badan mentah memakai rahasia URL itu, bandingkan dengan setiap v1, dan tolak stempel waktu yang lebih dari 5 menit (cegah pengiriman ulang oleh pihak lain). Saat rahasia dirotasi, dua v1 dikirim selama 24 jam.
Node.js
// Node.js (Express) — baca badan MENTAH, jangan JSON.parse dulu.
import crypto from "node:crypto";
import express from "express";
const RAHASIA = process.env.WEBHOOK_SECRET; // whsec_…
const app = express();
app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => {
const badan = req.body.toString("utf8");
const bagian = Object.fromEntries((req.get("X-Marsisten-Signature") || "").split(",").map((x) => x.split("=")));
const semuaV1 = (req.get("X-Marsisten-Signature") || "").split(",").filter((x) => x.startsWith("v1=")).map((x) => x.slice(3));
const t = Number(bagian.t);
if (!t || Math.abs(Date.now() / 1000 - t) > 300) return res.status(400).send("stempel waktu kedaluwarsa");
const harus = crypto.createHmac("sha256", RAHASIA).update(`${t}.${badan}`).digest("hex");
const cocok = semuaV1.some((v) => v.length === harus.length && crypto.timingSafeEqual(Buffer.from(v), Buffer.from(harus)));
if (!cocok) return res.status(401).send("tanda tangan salah");
res.sendStatus(200); // jawab cepat (≤ 10 detik), proses di belakang
const event = JSON.parse(badan);
// Cegah ganda: simpan event.id yang sudah diproses.
console.log(event.type, event.data);
});
app.listen(3000);PHP
<?php
// PHP — badan mentah dari php://input.
$rahasia = getenv('WEBHOOK_SECRET'); // whsec_…
$badan = file_get_contents('php://input');
$header = $_SERVER['HTTP_X_MARSISTEN_SIGNATURE'] ?? '';
$t = 0; $v1 = [];
foreach (explode(',', $header) as $b) {
[$k, $v] = array_pad(explode('=', $b, 2), 2, '');
if ($k === 't') $t = (int) $v;
if ($k === 'v1') $v1[] = $v;
}
if (!$t || abs(time() - $t) > 300) { http_response_code(400); exit('stempel waktu kedaluwarsa'); }
$harus = hash_hmac('sha256', $t . '.' . $badan, $rahasia);
$cocok = false;
foreach ($v1 as $v) { if (hash_equals($harus, $v)) { $cocok = true; } }
if (!$cocok) { http_response_code(401); exit('tanda tangan salah'); }
http_response_code(200);
$event = json_decode($badan, true);
// Cegah ganda: simpan $event['id'] yang sudah diproses.
error_log($event['type']);Python
# Python (Flask) — badan mentah dari request.get_data().
import hashlib, hmac, os, time
from flask import Flask, request, abort
RAHASIA = os.environ["WEBHOOK_SECRET"].encode() # whsec_…
app = Flask(__name__)
@app.post("/webhook")
def webhook():
badan = request.get_data()
bagian = request.headers.get("X-Marsisten-Signature", "").split(",")
t = next((int(x[2:]) for x in bagian if x.startswith("t=")), 0)
v1 = [x[3:] for x in bagian if x.startswith("v1=")]
if not t or abs(time.time() - t) > 300:
abort(400)
harus = hmac.new(RAHASIA, f"{t}.".encode() + badan, hashlib.sha256).hexdigest()
if not any(hmac.compare_digest(harus, v) for v in v1):
abort(401)
event = request.get_json()
# Cegah ganda: simpan event["id"] yang sudah diproses.
print(event["type"])
return "", 200Pengiriman ulang & penghentian#
- Sukses = jawaban 2xx dalam 10 detik. Redirect tidak diikuti.
- Gagal diulang otomatis: 1 menit, 5 menit, 30 menit, 2 jam, 6 jam, 12 jam.
- Event bisa terkirim lebih dari sekali — cegah ganda dengan field id.
- Gagal terus 3 hari → URL dinonaktifkan dan Anda diberi tahu lewat lonceng & email.
- Log kiriman disimpan 14 hari; kirim ulang dari tab Log.
Uji kirim#
Tombol Uji kirim di dasbor (atau POST /webhooks/{id}/test) mengirim event ping bertanda tangan ke URL Anda.