Preserve the original body
Signature checks must use the bytes the sender signed. Parsing JSON and then serializing it again can change whitespace or field order. Read the raw body first where the provider's signing method requires it.
HMAC verification pattern
For a configured HMAC-SHA256 contract, compute the expected signature with the shared secret and compare it in constant time. The contract must specify the exact payload, encoding, header, and timestamp behavior. This example illustrates the primitive; it does not replace a provider's documented verification method.
import { createHmac, timingSafeEqual } from "node:crypto";
const expected = createHmac("sha256", secret).update(rawBody).digest("hex");
const actual = suppliedSignature.replace(/^sha256=/, "");
const a = Buffer.from(expected);
const b = Buffer.from(actual);
const valid = a.length === b.length && timingSafeEqual(a, b);
Provider-specific contracts
Stripe webhooks use the configured Stripe signing secret and Stripe's verification helper. Twilio callbacks use Twilio's signature method, which includes the public callback URL and request parameters. Vapi callbacks must use the configured secret or signature contract implemented for that deployment. Do not treat a secret header as if it were an HMAC.
Reject before acting
Invalid or missing verification must fail before a transcript, subscription, credit balance, or operation status is changed. Do not log a complete secret or authentication header while diagnosing a failed check.
Replay and idempotency
Authentication and duplicate handling solve different problems. A correctly authenticated event may still be a retry. Record the provider event identifier and process the resulting state transition atomically where billing or credit changes are involved.
For a production review, confirm callback URLs, proxy behavior, clock assumptions where relevant, event retention, and the process for rotating a webhook secret.