Browse documentation

Webhooks

Webhook signature verification

Authenticate webhook requests against the raw request body before trusting their contents.

DevelopersAdvancedAvailable

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.

Signing formula
text
signed_payload = timestamp + "." + raw_request_body
digest = hex(HMAC_SHA256(signing_secret, signed_payload))
yanola-signature = "t=" + timestamp + ",v1=" + digest

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.

Verify before parsing
ts
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.

Rotation header for the fixture
text
yanola-signature: t=1785232800,v1=291aac08f6f256b8345612a3b4d00dcf0cbad23d918bad9e698d1924e11d996c,v1=c1d8c5c9e7f5faf0dc154bfe52c103261acac5f015afe61e4dcf73ac59c93583
Webhook signing test vector
InputValue
Secretwhsec_test
Timestamp1785232800
Raw body{"message":"Spacing is exact: ✓"}
v1 digest291aac08f6f256b8345612a3b4d00dcf0cbad23d918bad9e698d1924e11d996c

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

  1. Rotate the endpoint secret in the Yanola dashboard.
  2. Copy the new value and deploy it as YANOLA_WEBHOOK_SECRET.
  3. During the grace period, Yanola includes digests for the active and retiring secrets.
  4. The SDK checks every bounded v1 candidate, so either deployed secret can validate during rollout.
  5. After the previous-secret expiry shown in the dashboard, remove the old secret from deployment configuration.

Raw-body framework rules

Raw-body access by framework
FrameworkRequired pattern
ExpressRegister express.raw({ type: 'application/json' }) on the webhook route before express.json(); req.body is a Buffer.
Next.js App RouterCall await request.text() exactly once, verify that string, then JSON.parse the same string.
NestJS with ExpressCreate the app with { rawBody: true } and read RawBodyRequest<Request>.rawBody.

Verification failure policy

  1. Reject a missing, malformed, oversized, or stale yanola-signature.
  2. Reject the request when no v1 digest matches in constant time.
  3. Do not parse, log, queue, or process the unverified payload.
  4. Return a non-2xx response such as 400 so the failure appears in delivery history.
  5. Log only safe correlation headers such as yanola-event-id and yanola-delivery-id; never log the signing secret or full signature.