Inbound

Webhook Signature Verification

Verify HMAC-signed email webhooks before processing delivery, bounce, complaint, and inbound email events.

Updated

TL;DR

Verify webhook signatures against the raw request body before parsing JSON. Use constant-time comparison, reject stale timestamps, and make the handler idempotent because providers retry webhooks.

What you will learn

  • Verify webhook authenticity safely
  • Avoid JSON reserialization bugs
  • Combine signature checks with idempotent processing

What signatures prove

A webhook signature proves that the request body was produced by a party with the shared signing secret. It does not prove that your application has not already processed the event.

Use signatures for authenticity and integrity. Use idempotency for duplicate safety.

Verification sequence

  1. Read the raw request body.
  2. Read the timestamp and signature headers.
  3. Reject timestamps outside your tolerance window.
  4. Build the signed payload exactly as documented.
  5. Compute HMAC-SHA256 with the webhook signing secret.
  6. Compare expected and received signatures in constant time.
  7. Only then parse JSON and process the event.

Do not log the signing secret. Do not put the secret in client-side code.

Node example

import crypto from "node:crypto";

function verifyPostscaleWebhook({
  rawBody,
  timestamp,
  signature,
  secret,
}: {
  rawBody: Buffer;
  timestamp: string;
  signature: string;
  secret: string;
}) {
  const ageSeconds = Math.abs(Date.now() / 1000 - Number(timestamp));
  if (!Number.isFinite(ageSeconds) || ageSeconds > 300) {
    return false;
  }

  const signedPayload = Buffer.concat([
    Buffer.from(timestamp),
    Buffer.from("."),
    rawBody,
  ]);

  const expected = crypto
    .createHmac("sha256", secret)
    .update(signedPayload)
    .digest("hex");

  return crypto.timingSafeEqual(
    Buffer.from(expected, "hex"),
    Buffer.from(signature, "hex")
  );
}

Adjust header names and signed payload format to match your provider's documentation.

Handler checklist

  1. Keep the raw body available in your framework.
  2. Disable automatic body parsing for the webhook route if needed.
  3. Reject missing signatures.
  4. Reject stale timestamps.
  5. Compare in constant time.
  6. Store event IDs before side effects.
  7. Return a 2xx response only after durable processing or durable enqueue.

Use webhook retries and idempotency for the processing side.

Common failures

FailureCauseFix
Signature mismatchVerifying parsed JSONVerify raw bytes
Valid old request replayedNo timestamp checkEnforce a short tolerance window
Duplicate status updatesRetried webhookStore event IDs
Lost event after crashSide effect before durable writeEnqueue or write first

References

Frequently asked questions

Should I verify the parsed JSON body?
No. Verify the exact raw request body. JSON parsing and reserialization can change whitespace or field order and break the signature.
Why do I need idempotency if signatures are valid?
A valid webhook can still be delivered more than once because retries are normal. Store event IDs and ignore duplicates.

Related guides

Put the guide into production

Postscale brings sending, inbound processing, DMARC reporting, and masked addresses behind one API so the operational pieces stay connected.