HostMentor Docs

Webhooks

Receive incoming WhatsApp messages and delivery statuses on your webhook, verify the X-HostMentor-Signature header, and handle retries and duplicates.

Your webhook is a URL on your server that receives events from WhatsApp:

  • Incoming messages from your customers, including their button and list choices.
  • Message statuses: when your messages are sent, delivered and read, or fail.
  • Template updates: approvals, rejections, pauses and quality changes.
  • Account updates: your number's name and quality rating, messaging limits and restrictions.

Webhook events lists every event with its payload and examples.

Set up your webhook

  1. Build an HTTPS endpoint that accepts POST requests with a JSON body.
  2. Add its URL at app.hostmentor.com under Messaging → WhatsApp → API keys and webhook. The dashboard shows your signing secret, which starts with whsec_. Use Send test event there to check your endpoint; it sends a signed messages status event for the message ID wamid.TEST.
  3. Check the signature on every request (below), then respond with HTTP 200 within 10 seconds. Do slow work, such as calling other systems, after responding.

A number has exactly one webhook, managed by HostMentor. The API can't change it, and attempts fail with webhook_managed_by_hostmentor.

Check the signature

Every webhook request carries an X-HostMentor-Signature header:

X-HostMentor-Signature: t=1727430000,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd

t is when we sent it, as a Unix timestamp. v1 is an HMAC-SHA256 of t, a full stop and the raw request body, signed with your signing secret. Reject the request if the signature doesn't match, or if t is more than 5 minutes old. That protects you from forged and replayed requests.

Compute the signature over the body exactly as received, before parsing the JSON. Parsing and re-encoding it changes the bytes.

Right after you rotate your signing secret, the header carries two v1 values for 24 hours, one for each secret. Accept the request if either matches, so you can switch secrets without missing events.

Node.js

import crypto from "node:crypto";

// rawBody: the request body exactly as received (string or Buffer).
export function isValidSignature(rawBody, header, secret) {
  const parts = header.split(",").map((kv) => kv.split("="));
  const t = parts.find(([key]) => key === "t")?.[1];
  if (!t || Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
  const expected = Buffer.from(crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex"));
  return parts.some(
    ([key, value]) => key === "v1" && value?.length === expected.length && crypto.timingSafeEqual(Buffer.from(value), expected),
  );
}

Python

import hashlib, hmac, time

def is_valid_signature(raw_body: bytes, header: str, secret: str) -> bool:
    parts = [item.split("=", 1) for item in header.split(",") if "=" in item]
    t = next((value for key, value in parts if key == "t"), "")
    if not t.isdigit() or abs(time.time() - int(t)) > 300:
        return False
    expected = hmac.new(secret.encode(), t.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
    return any(key == "v1" and hmac.compare_digest(expected, value) for key, value in parts)

PHP

function is_valid_signature(string $rawBody, string $header, string $secret): bool {
    $t = null;
    $signatures = [];
    foreach (explode(',', $header) as $part) {
        [$key, $value] = array_pad(explode('=', $part, 2), 2, '');
        if ($key === 't') $t = $value;
        if ($key === 'v1') $signatures[] = $value;
    }
    if ($t === null || !ctype_digit($t) || abs(time() - (int) $t) > 300) return false;
    $expected = hash_hmac('sha256', $t . '.' . $rawBody, $secret);
    foreach ($signatures as $signature) {
        if (hash_equals($expected, $signature)) return true;
    }
    return false;
}
// $rawBody = file_get_contents('php://input');

Retries and duplicates

  • If your endpoint doesn't answer with a 2xx status within 10 seconds, we retry with increasing delays for up to 3 days. After that we turn the webhook off and email your team.
  • The same event can arrive more than once. Use the message ID and status to skip ones you've already handled.
  • Events can arrive out of order. For example, read can arrive before delivered. Use each event's timestamp.

On this page