Advanced7 min read

Verify webhook signatures

Validate the X-Forge-Signature header using HMAC-SHA256 to confirm webhook authenticity in Python and Node.js.

Forge signs every outbound webhook delivery with HMAC-SHA256. Verifying the signature on your server confirms the request came from Forge and that the body was not tampered with in transit.

How the signature works

Forge takes the raw JSON body bytes and computes HMAC-SHA256(secret, body). The result is included in the X-Forge-Signature: sha256=<hex> request header. Your secret is unique per webhook and is available in Settings → Integrations → Webhooks — click "Reveal" next to the webhook.

Python

import hmac
import hashlib

def verify_forge_signature(
    body: bytes,
    signature_header: str,
    secret: str
) -> bool:
    expected = hmac.new(
        secret.encode(),
        body,
        hashlib.sha256
    ).hexdigest()
    received = signature_header.removeprefix("sha256=")
    # Use compare_digest to prevent timing attacks
    return hmac.compare_digest(expected, received)

# Flask example
@app.route("/forge-webhook", methods=["POST"])
def webhook():
    body = request.get_data()
    sig  = request.headers.get("X-Forge-Signature", "")
    if not verify_forge_signature(body, sig, FORGE_SECRET):
        abort(401)
    payload = request.json
    # handle payload...
    return "ok"

Node.js / TypeScript

import { createHmac, timingSafeEqual } from "crypto"

function verifyForgeSignature(
  body: string | Buffer,
  signatureHeader: string,
  secret: string
): boolean {
  const sig = createHmac("sha256", secret)
    .update(body)
    .digest("hex")
  const received = signatureHeader.replace("sha256=", "")
  // timingSafeEqual prevents timing-based attacks
  return timingSafeEqual(
    Buffer.from(sig, "hex"),
    Buffer.from(received, "hex")
  )
}

// Express example
app.post("/forge-webhook", express.raw({ type: "application/json" }), (req, res) => {
  const sig = req.headers["x-forge-signature"] as string
  if (!verifyForgeSignature(req.body, sig, process.env.FORGE_SECRET!)) {
    return res.status(401).send("Unauthorized")
  }
  const payload = JSON.parse(req.body.toString())
  // handle payload...
  res.send("ok")
})
Tip:Always use a constant-time comparison function (hmac.compare_digest in Python, timingSafeEqual in Node.js). A regular string equality check (===) is vulnerable to timing attacks that can leak the expected signature bit by bit.

Rotating your secret

If your secret is ever exposed, delete the webhook from Settings → Integrations → Webhooks and add a new one. A new secret is generated automatically. Update the secret in your server environment and redeploy.

Verify webhook signatures — Forge Guides — Forge