Skip to content

Verify Meta (WhatsApp, Facebook, Instagram) webhooks

Verify that a webhook really came from Meta (WhatsApp, Facebook, Instagram) before acting on it.

Identifier 'meta', 'whatsapp', 'facebook', 'instagram'
Import import { verifyWhatsApp } from 'verihook/meta'
Headers x-hub-signature-256
Signature HMAC-SHA256 of the raw body, keyed with the app secret, sent as sha256=<hex>.
Replay window None (the provider sends no timestamp)
result.eventType the body’s object, e.g. whatsapp_business_account

Your app’s App Secret: in the Meta App Dashboard, open App settings → Basic. The verify token used in the setup handshake is a different value that you choose.

Store it in an environment variable (META_APP_SECRET below) and never commit it.

import { verifyWhatsApp } from 'verihook/meta';
export async function POST(request: Request) {
const result = await verifyWhatsApp(request, process.env.META_APP_SECRET!);
if (!result.valid) {
console.warn(result.code, result.reason, result.hint);
return new Response('Invalid signature', { status: 401 });
}
console.log(result.eventType, result.event);
return Response.json({ received: true });
}

request can be a Fetch Request or { headers, body, url? } with the raw body. verifyWebhook('meta', request, secret) from verihook does the same. Read why the raw body matters if verification fails behind a body parser.

app/api/webhooks/meta/route.ts
import { createWebhookHandler } from 'verihook/next';
export const POST = createWebhookHandler('meta', process.env.META_APP_SECRET!, async (payload, result) => {
// Runs only for a valid signature
console.log(result.eventType, result.event);
});

Each adapter responds 401 to an invalid signature, 413 to a body over maxBodySize (2 MB by default) and 200 to a duplicate when you pass a dedupeStore.

Meta checks the URL with a GET request before sending events. Answer it with verifyMetaChallenge:

import { verifyMetaChallenge } from 'verihook/meta';
// GET /webhooks/whatsapp
export function GET(request: Request) {
const result = verifyMetaChallenge(new URL(request.url), process.env.META_VERIFY_TOKEN!);
return result.valid
? new Response(result.challenge)
: new Response('Forbidden', { status: 403 });
}
  • 'meta', 'whatsapp', 'facebook' and 'instagram' are the same verifier, so verifyMeta and verifyWhatsApp are interchangeable.

signWebhook from verihook/testing builds a correctly signed Meta (WhatsApp, Facebook, Instagram) request for your tests:

import { signWebhook } from 'verihook/testing';
import { verifyWhatsApp } from 'verihook/meta';
const hook = await signWebhook('meta', {
secret: 'meta_app_secret',
payload: { id: 'evt_test' },
});
const result = await verifyWhatsApp(
{ headers: hook.headers, body: hook.body, url: hook.url },
hook.secret,
);
// result.valid === true

From the command line, npx verihook simulate meta --url http://localhost:3000/webhooks/meta sends one to your local server.

See Testing your handlers for supertest and Fetch Request examples.

Failed results carry a code and often a hint with the likely cause. See Troubleshooting for each error code.

Meta (WhatsApp, Facebook, Instagram)’s webhook documentation