Follow this pattern when your server receives WhatsApp events from Wazapin.
1

Verify signature

Validate incoming signature on the raw body bytes before processing to secure your endpoint. See Webhook signature verification.
2

Parse and validate payload

Convert the verified raw payload to JSON and check the event direction (direction: "inbound" vs direction: "outbound").
3

Deduplicate events

Use the svix-id (or webhook-id) HTTP header as an idempotency key. Check your database/cache to ensure you have not processed this event ID before.
4

Acknowledge quickly

Return a 200 OK response to Wazapin within 3 seconds to prevent delivery retries. Queue the event payload for asynchronous processing.
5

Process asynchronously

Inspect msg_type or event types and dispatch to the correct task handler (text, media, interactive buttons, or delivery receipts).

Code example

Here is a template to receive, verify, and route webhook events:
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. Verify webhook signature
  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. Deduplicate using event ID
  if (await isAlreadyProcessed(eventId)) {
    return res.status(200).send("Duplicate acknowledged");
  }

  // 3. Acknowledge receipt quickly (within 3 seconds)
  res.status(200).send("OK");

  // 4. Process event asynchronously (outside request cycle)
  processEventAsync(payload, eventId).catch(console.error);
});

async function isAlreadyProcessed(id: string): Promise<boolean> {
  // Check your database/cache (e.g. Redis) here
  return false; 
}

async function processEventAsync(payload: any, eventId: string) {
  // Mark event as processed in your store to ensure idempotency
  await markAsProcessed(eventId);

  // Route by event properties
  if (payload.status) {
    // Delivery status update receipt
    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);
    }
  }
}

// Stubs for task guides
async function handleInboundText(payload: any) { /* See guide */ }
async function handleInboundMedia(payload: any) { /* See guide */ }
async function handleInteractiveReplies(payload: any) { /* See guide */ }
async function handleDeliveryStatus(payload: any) { /* See guide */ }
async function markAsProcessed(id: string) { /* Save state */ }

Route guides

Once your skeleton handler is set up, implement the business logic using these detailed guides: