How to Verify Webhook Signatures

Every JsonHook delivery includes an HMAC-SHA256 signature. Verifying it ensures only JsonHook can deliver to your endpoint — protecting against spoofed or tampered webhook requests.

Table of Contents
  1. Overview
  2. Prerequisites
  3. Step-by-Step Instructions
  4. Code Example
  5. Common Pitfalls

Overview

Webhook signature verification is a security mechanism that proves a webhook request was sent by JsonHook (and not by an attacker who knows your endpoint URL). Without it, anyone who discovers your webhook URL can POST arbitrary payloads that your handler will process as legitimate email deliveries.

JsonHook uses HMAC-SHA256 signatures. When an inbound address is created, a shared secret is generated and returned in the API response. JsonHook uses this secret to compute an HMAC-SHA256 digest of the raw request body for every delivery, and includes it in the X-JsonHook-Signature request header.

Your webhook handler must:

  1. Read the raw request body before any parsing
  2. Compute HMAC-SHA256(secret, rawBody)
  3. Compare the result to the X-JsonHook-Signature header value
  4. Reject (return 401) if they do not match
  5. Only then parse and process the payload

Prerequisites

To implement signature verification you need:

  • Your webhook secret — returned when you created the inbound address via POST /v1/addresses. Store it as an environment variable (JSONHOOK_SECRET).
  • A web framework that provides access to the raw (unparsed) request body bytes — this is critical. If a JSON middleware has already parsed the body, you cannot reconstruct the exact bytes that were signed.
  • A standard HMAC-SHA256 implementation — available in the standard library of every major programming language.

If you have lost your webhook secret, you can rotate it via POST /v1/addresses/{id}/rotate-secret. The new secret takes effect immediately, so update your environment variable promptly.

Secure Your Webhook Endpoint

HMAC-SHA256 signatures on every delivery. Built in on all plans.

Get Free API Key

Step-by-Step Instructions

Implement signature verification in 4 steps:

  1. Configure raw body access. Make sure your framework gives you the raw request body bytes before any parsing middleware runs. In Express, add a raw body parser for the webhook route. In Django, use request.body. In Flask, use request.data.
  2. Read the signature header:
    const signature = req.headers["x-jsonhook-signature"];
    If the header is absent, reject the request immediately.
  3. Compute the expected signature:
    const expected = crypto
      .createHmac("sha256", process.env.JSONHOOK_SECRET)
      .update(rawBody)
      .digest("hex");
  4. Compare using a timing-safe function:
    if (!crypto.timingSafeEqual(
      Buffer.from(signature),
      Buffer.from(expected)
    )) {
      return res.status(401).send("Invalid signature");
    }
    Use a timing-safe comparison to prevent timing oracle attacks. Standard string equality (===) leaks timing information.

Code Example

Multi-language signature verification examples:

Node.js:

import crypto from "crypto";
function verifySignature(rawBody: Buffer, secret: string, sig: string): boolean {
  const expected = crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
  return crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
}

Python:

import hmac, hashlib
def verify_signature(raw_body: bytes, secret: str, sig: str) -> bool:
    expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(sig, expected)

Go:

import ("crypto/hmac"; "crypto/sha256"; "encoding/hex")
func verifySignature(rawBody []byte, secret, sig string) bool {
    mac := hmac.New(sha256.New, []byte(secret))
    mac.Write(rawBody)
    expected := hex.EncodeToString(mac.Sum(nil))
    return hmac.Equal([]byte(sig), []byte(expected))
}

Ruby:

require "openssl"
def verify_signature(raw_body, secret, sig)
  expected = OpenSSL::HMAC.hexdigest("SHA256", secret, raw_body)
  ActiveSupport::SecurityUtils.secure_compare(sig, expected)
end

Common Pitfalls

Signature verification frequently fails due to these mistakes:

  • Body already parsed by middleware. If express.json() or equivalent runs before your handler, the raw bytes are gone and your HMAC will not match. Configure a dedicated raw body parser for the webhook route only.
  • Comparing with standard string equality. Use timingSafeEqual (Node.js), hmac.compare_digest (Python), hmac.Equal (Go), or secure_compare (Ruby). Regular equality is vulnerable to timing attacks.
  • Encoding mismatch. Both the received signature and your computed signature must be hex-encoded strings before comparison. Do not mix hex and base64.
  • Extra whitespace or newlines added to the body. Middleware that reformats or pretty-prints the JSON body will break the HMAC. Always verify against the exact bytes received over the wire.
  • Wrong secret. Ensure you are using the secret from the specific address the delivery was sent to. If you have multiple addresses, each has its own secret.

Frequently Asked Questions

What algorithm does JsonHook use for webhook signatures?

JsonHook uses HMAC-SHA256. The signature is the hex-encoded HMAC-SHA256 digest of the raw request body, computed using the address's webhook secret as the key. It is delivered in the X-JsonHook-Signature request header.

Can I rotate my webhook secret without downtime?

Yes. Call POST /v1/addresses/{id}/rotate-secret to generate a new secret. For zero-downtime rotation, JsonHook supports a brief dual-signature period: after rotation, it sends both the old and new signatures in the request for a configurable overlap window (default: 5 minutes) so your handler can verify against either.

What should I do if signature verification fails?

Return a 401 response and log the failure with the delivery ID, remote IP, and timestamp. Do not process the payload. Investigate the failure — it may indicate an attacker probing your endpoint, a misconfigured secret, or a body-parsing middleware issue. Frequent 401s from JsonHook's IP ranges indicate a configuration problem rather than an attack.

Is signature verification required on all plans?

Signature verification is available on all plans including the free tier. It is strongly recommended for all production deployments. JsonHook does not offer unsigned delivery as an option — security is built in by default.