Skip to content

Contoh: tangani event webhook

Verifikasi signature, deduplikasi pengiriman, dan proses event tanpa kehilangan pesan.

Updated View as Markdown

Ikuti pola ini pas server kamu terima event WhatsApp dari Wazapin.

Verifikasi signature

Validasi signature yang masuk dari raw body bytes sebelum kamu proses apa pun. Ini buat amankan endpoint kamu. Lihat Verifikasi signature webhook.

Parse dan validasi payload

Ubah raw payload yang sudah terverifikasi jadi JSON, lalu cek arah event (direction: "inbound" vs direction: "outbound").

Deduplikasi event

Pakai header HTTP svix-id (atau webhook-id) sebagai idempotency key. Cek database/cache kamu — pastiin event dengan ID ini belum pernah kamu proses sebelumnya.

Balas cepat

Balas 200 OK ke Wazapin dalam 3 detik biar nggak di-retry. Masukkan payload event ke queue buat diproses async.

Proses secara async

Cek msg_type atau tipe event, lalu lempar ke handler yang tepat (teks, media, tombol interaktif, atau delivery receipt).


Contoh kode

Template buat terima, verifikasi, dan routing event webhook:

import express from "express";
import { Webhook } from "svix";

const app = express();
const wh = new Webhook(process.env.WAZAPIN_WEBHOOK_SECRET!);

app.post("/webhooks/wazapin", express.raw({ type: "application/json" }), async (req, res) => {
  // 1. Verifikasi signature webhook
  try {
    wh.verify(req.body, req.headers as Record<string, string>);
  } catch (err) {
    return res.status(403).send("Invalid signature");
  }

  const payload = JSON.parse(req.body.toString("utf8"));
  const eventId = req.headers["svix-id"] as string;

  // 2. Deduplikasi pakai event ID
  if (await isAlreadyProcessed(eventId)) {
    return res.status(200).send("Duplicate acknowledged");
  }

  // 3. Balas cepat (dalam 3 detik)
  res.status(200).send("OK");

  // 4. Proses event secara async (di luar request cycle)
  processEventAsync(payload, eventId).catch(console.error);
});

async function isAlreadyProcessed(id: string): Promise<boolean> {
  // Cek database/cache kamu (mis. Redis) di sini
  return false; 
}

async function processEventAsync(payload: any, eventId: string) {
  // Tandai event sudah diproses biar idempotent
  await markAsProcessed(eventId);

  // Routing berdasarkan properti event
  if (payload.status) {
    // Update status pengiriman
    return handleDeliveryStatus(payload);
  }

  if (payload.direction === "inbound") {
    switch (payload.msg_type) {
      case "text":
        return handleInboundText(payload);
      case "image":
      case "video":
      case "audio":
      case "document":
      case "sticker":
        return handleInboundMedia(payload);
      case "interactive":
        return handleInteractiveReplies(payload);
      default:
        console.log("Unhandled inbound message type:", payload.msg_type);
    }
  }
}

// Stub buat guide tugas
async function handleInboundText(payload: any) { /* Lihat guide */ }
async function handleInboundMedia(payload: any) { /* Lihat guide */ }
async function handleInteractiveReplies(payload: any) { /* Lihat guide */ }
async function handleDeliveryStatus(payload: any) { /* Lihat guide */ }
async function markAsProcessed(id: string) { /* Simpan state */ }
from fastapi import FastAPI, Request, HTTPException, BackgroundTasks
from svix.webhooks import Webhook, WebhookVerificationError
import os
import json

app = FastAPI()
wh = Webhook(os.environ["WAZAPIN_WEBHOOK_SECRET"])

@app.post("/webhooks/wazapin")
async def handle_webhook(request: Request, background_tasks: BackgroundTasks):
    body = await request.body()
    headers = dict(request.headers)
    event_id = headers.get("svix-id")

    # 1. Verifikasi signature
    try:
        wh.verify(body, headers)
    except WebhookVerificationError:
        raise HTTPException(status_code=403, detail="Invalid signature")

    # 2. Cek duplikat
    if is_already_processed(event_id):
        return {"ok": True, "detail": "Duplicate"}

    payload = json.loads(body.decode("utf-8"))

    # 3. Jadwalkan proses async & balas 200 OK
    background_tasks.add_task(process_event_async, payload, event_id)
    return {"ok": True}

def is_already_processed(event_id: str) -> bool:
    # Cek Redis/DB di sini
    return False

def process_event_async(payload: dict, event_id: str):
    mark_as_processed(event_id)
    
    # Routing event
    status = payload.get("status")
    direction = payload.get("direction")
    msg_type = payload.get("msg_type")

    if status:
        handle_delivery_status(payload)
    elif direction == "inbound":
        if msg_type == "text":
            handle_inbound_text(payload)
        elif msg_type in {"image", "video", "audio", "document", "sticker"}:
            handle_inbound_media(payload)
        elif msg_type == "interactive":
            handle_interactive_replies(payload)

def handle_inbound_text(payload): pass
def handle_inbound_media(payload): pass
def handle_interactive_replies(payload): pass
def handle_delivery_status(payload): pass
def mark_as_processed(event_id): pass

Panduan lanjutan

Kalau skeleton handler kamu sudah jadi, lanjut isi logika bisnisnya pakai guide detail ini:

Navigation

Type to search…

↑↓ navigate↵ selectEsc close