Webhooks
Webhook signature verification
Authenticate webhook requests against the raw request body before trusting their contents.
What this guide covers
- Read the raw body before JSON parsing changes its byte representation.
- Compare the expected and received signatures with a timing-safe operation.
- Reject missing, malformed, or invalid signatures before side effects.
Production signing contract
Yanola computes each v1 digest as lowercase hex HMAC-SHA256 over the UTF-8 bytes of <timestamp>.<exact raw body>, using the endpoint signing secret.
The yanola-signature header is t=<unix_seconds>,v1=<digest>. During secret rotation it can include multiple comma-separated v1 values; verify all supported v1 candidates without returning early.
signed_payload = timestamp + "." + raw_request_body
digest = hex(HMAC_SHA256(signing_secret, signed_payload))
yanola-signature = "t=" + timestamp + ",v1=" + digestJSON whitespace, escaping, property order, and Unicode bytes are part of the signature. Parsing and re-serializing the body before verification changes the signed input.
Verify with the SDK
verifyYanolaWebhookSignature performs bounded header parsing, constant-time digest comparison, rotation-signature checks, and timestamp validation. It accepts a string, Buffer, or Uint8Array raw body.
import {
verifyYanolaWebhookSignature,
} from "@yanola/whatsapp"
const secret = process.env.YANOLA_WEBHOOK_SECRET
if (!secret) throw new Error("Missing YANOLA_WEBHOOK_SECRET")
const rawBody = await request.text()
const valid = verifyYanolaWebhookSignature({
secret,
signature: request.headers.get("yanola-signature"),
rawBody,
})
if (!valid) {
return new Response("Invalid Yanola signature", { status: 400 })
}
const event = JSON.parse(rawBody)Production test vector
This fixture matches the backend production signing test, including the Unicode checkmark and exact compact JSON bytes.
yanola-signature: t=1785232800,v1=291aac08f6f256b8345612a3b4d00dcf0cbad23d918bad9e698d1924e11d996c,v1=c1d8c5c9e7f5faf0dc154bfe52c103261acac5f015afe61e4dcf73ac59c93583| Input | Value |
|---|---|
Secret | whsec_test |
Timestamp | 1785232800 |
Raw body | {"message":"Spacing is exact: ✓"} |
v1 digest | 291aac08f6f256b8345612a3b4d00dcf0cbad23d918bad9e698d1924e11d996c |
Reject replayed timestamps
The SDK enforces a five-minute tolerance in both directions by default. A cryptographically correct signature outside the tolerance is rejected to limit captured-request replay.
Keep application servers synchronized with a reliable clock. You can set toleranceSeconds explicitly, but widening it increases replay exposure; disabling the check with a very large value is unsafe.
Handle secret rotation
- Rotate the endpoint secret in the Yanola dashboard.
- Copy the new value and deploy it as YANOLA_WEBHOOK_SECRET.
- During the grace period, Yanola includes digests for the active and retiring secrets.
- The SDK checks every bounded v1 candidate, so either deployed secret can validate during rollout.
- After the previous-secret expiry shown in the dashboard, remove the old secret from deployment configuration.
Raw-body framework rules
| Framework | Required pattern |
|---|---|
Express | Register express.raw({ type: 'application/json' }) on the webhook route before express.json(); req.body is a Buffer. |
Next.js App Router | Call await request.text() exactly once, verify that string, then JSON.parse the same string. |
NestJS with Express | Create the app with { rawBody: true } and read RawBodyRequest<Request>.rawBody. |
Verification failure policy
- Reject a missing, malformed, oversized, or stale yanola-signature.
- Reject the request when no v1 digest matches in constant time.
- Do not parse, log, queue, or process the unverified payload.
- Return a non-2xx response such as 400 so the failure appears in delivery history.
- Log only safe correlation headers such as yanola-event-id and yanola-delivery-id; never log the signing secret or full signature.