Lewati ke isi
MarsistenDocs

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.receivedPesan masuk dari pelanggan (semua jenis, dengan tautan media sementara).
message.statusStatus pesan keluar berubah: terkirim, diterima, dibaca, atau gagal.
message.sentPesan keluar dari mana pun: CS, AI Agent, automasi, broadcast, atau API.
conversation.assignedPercakapan ditugaskan ke anggota tim atau dilepas.
conversation.closedPercakapan ditandai selesai.
ai.handoverAI Agent menyerahkan percakapan ke CS manusia.
channel.connectedNomor WhatsApp tersambung.
channel.disconnectedNomor WhatsApp terputus.
broadcast.finishedBroadcast selesai, dihentikan, atau gagal — dengan ringkasan angka.
contact.createdKontak WhatsApp baru tersimpan (chat masuk, impor, Inbox, atau API).
contact.taggedTag baru ditambahkan ke kontak WhatsApp (berisi tag yang ditambahkan).
order.createdPesanan baru dibuat (Inbox, AI Agent, tagihan, atau checkout halaman toko).
order.paidPesanan lunas (Mayar, transfer dikonfirmasi, atau mutasi terdeteksi otomatis).
order.shippedPesanan dikirim — kurir & resi diisi.
ticket.createdTiket baru dibuat oleh tim atau AI Agent.
csat.submittedPelanggan 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 "", 200

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