Webhooks

Receiving events

wapi-cloud provides HMAC-SHA256 signature verification, webhook challenge handshakes, normalized event parsing, and event dispatcher utilities for Express, Next.js, and Fastify.

1. Express One-Liner (handleExpress)

For Express servers, whatsapp.webhooks.handleExpress automatically wires up both the GET verification challenge and the POST signature verification/event dispatcher in a single call:

server.ts
import express from "express";
import { Whatsapp } from "wapi-cloud";

const app = express();
const whatsapp = new Whatsapp({
  accessToken: process.env.WA_TOKEN!,
  phoneNumberId: process.env.WA_PHONE_ID!,
  appSecret: process.env.WA_APP_SECRET!,
});

// Setup webhook route and verifyToken challenge
whatsapp.webhooks.handleExpress(app, "/webhook", {
  verifyToken: process.env.WA_VERIFY_TOKEN!,
});

// Listen for incoming customer messages
whatsapp.webhooks.on("message", async (msg) => {
  if (msg.messageType === "text") {
    console.log("Received text from " + msg.from + ": " + msg.text.body);
    await whatsapp.messages.sendText(msg.from, {
      body: "Echo: " + msg.text.body,
    });
  } else if (msg.messageType === "image") {
    console.log("Received photo from " + msg.from + ", mediaId: " + msg.image.id);
  }
});

// Listen for delivery status updates (sent, delivered, read, failed)
whatsapp.webhooks.on("status", (status) => {
  console.log("Message " + status.messageId + " to " + status.recipientId + ": " + status.status);
});

// Listen for template approval or rejection updates
whatsapp.webhooks.on("template_status_update", (t) => {
  console.log("Template " + t.messageTemplateName + " updated to " + t.event);
});

app.listen(3000, () => console.log("Server listening on :3000"));

2. Next.js App Router (Route Handler)

In modern Next.js applications (App Router app/api/webhook/route.ts), use verifyHandshake and verifySignature:

app/api/webhook/route.ts
import { whatsapp } from "@/lib/whatsapp";

// 1. GET: Webhook verification challenge
export async function GET(req: Request) {
  const { searchParams } = new URL(req.url);

  try {
    const challenge = whatsapp.webhooks.verifyHandshake({
      mode: searchParams.get("hub.mode"),
      token: searchParams.get("hub.verify_token"),
      challenge: searchParams.get("hub.challenge"),
      expectedToken: process.env.WA_VERIFY_TOKEN!,
    });
    return new Response(challenge, { status: 200 });
  } catch {
    return new Response("Forbidden", { status: 403 });
  }
}

// 2. POST: Inbound messages and status receipts
export async function POST(req: Request) {
  const rawBody = await req.text();
  const signatureHeader = req.headers.get("x-hub-signature-256") ?? undefined;

  // Verify HMAC-SHA256 signature
  const isValid = whatsapp.webhooks.verifySignature({
    payload: rawBody,
    signatureHeader,
  });

  if (!isValid) {
    return new Response("Invalid signature", { status: 401 });
  }

  // Parse events into strongly-typed array
  const events = whatsapp.webhooks.parse(JSON.parse(rawBody));

  for (const event of events) {
    if (event.type === "message" && event.messageType === "text") {
      await whatsapp.messages.sendText(event.from, {
        body: "Hello! Received: " + event.text.body,
      });
    }
  }

  return new Response("OK", { status: 200 });
}

3. Manual Express Handler

manual-server.ts
// Mount express.raw on the webhook path to preserve unmodified byte payload for HMAC
app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => {
  const isValid = whatsapp.webhooks.verifySignature({
    payload: req.body,
    signatureHeader: req.headers["x-hub-signature-256"],
  });

  if (!isValid) return res.sendStatus(401);

  const json = JSON.parse(req.body.toString("utf-8"));
  const events = whatsapp.webhooks.parse(json);

  for (const event of events) {
    if (event.type === "message") {
      console.log("Inbound message from:", event.from);
    }
  }

  res.sendStatus(200);
});

Event Types

ParameterTypeDescription
messageInboundMessageEventInbound message from a user. Discriminated by messageType: 'text' | 'image' | 'video' | 'audio' | 'document' | 'sticker' | 'location' | 'contacts' | 'interactive' | 'button' | 'reaction' | 'unknown'.
statusMessageStatusEventDelivery receipt status for an outbound message: 'sent' | 'delivered' | 'read' | 'failed'. Includes timestamp and errors if failed.
template_status_updateTemplateStatusUpdateEventNotification that a message template was APPROVED, REJECTED, PAUSED, or DISABLED by Meta.
account_alertAccountAlertEventSystem alerts regarding WABA health, restriction status, or payment alerts.