---
title: "Example: handle webhook events"
description: "Verify signatures, deduplicate deliveries, and process events without dropping messages."
---

> Documentation Index
> Fetch the complete documentation index at: https://docs.wazapin.id/llms.txt
> Use this file to discover all available pages before exploring further.

# Example: handle webhook events

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](/api/webhook-signature-examples).

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:

### TypeScript (Express)

```typescript
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 */ }
```
### Python (FastAPI)

```python
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. Verify signature
try:
    wh.verify(body, headers)
except WebhookVerificationError:
    raise HTTPException(status_code=403, detail="Invalid signature")

# 2. Check for duplicates
if is_already_processed(event_id):
    return {"ok": True, "detail": "Duplicate"}

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

# 3. Schedule task to process asynchronously & return 200 OK
background_tasks.add_task(process_event_async, payload, event_id)
return {"ok": True}

def is_already_processed(event_id: str) -> bool:
# Check Redis/DB here
return False

def process_event_async(payload: dict, event_id: str):
mark_as_processed(event_id)

# Route 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
```

---

## Route guides
Once your skeleton handler is set up, implement the business logic using these detailed guides:
- [Handle inbound text](/guides/handle-inbound-text)
- [Handle inbound media](/guides/handle-inbound-media)
- [Handle interactive replies](/guides/handle-interactive-replies)
- [Handle delivery status updates](/guides/handle-delivery-status)

Source: https://docs.wazapin.id/recipes/handle-webhook-events/index.mdx
