Every webhook request includes an X-Arowana-Signature header. Always verify it before processing the payload to ensure the request came from Arowana and wasn’t tampered with.

How it works

1

Read the raw request body

Use the exact HTTP body bytes. Do not parse and re-serialize JSON before verifying — this changes the byte sequence and breaks the signature.
2

Compute HMAC-SHA256

Sign the raw body bytes with your webhook secret using HMAC-SHA256. The expected value is sha256= followed by the hex digest.
3

Compare with constant-time equality

Use a constant-time comparison to prevent timing attacks. Return 401 on mismatch — do not process the payload.

Implementation

const crypto = require("crypto");
const express = require("express");

function verifySignature(rawBody, secret, signature) {
  const expected = "sha256=" + crypto
    .createHmac("sha256", secret)
    .update(rawBody)
    .digest("hex");
  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(signature)
  );
}

app.post(
  "/webhooks/arowana",
  express.raw({ type: "application/json" }),
  (req, res) => {
    const sig = req.headers["x-arowana-signature"];
    if (!verifySignature(req.body, process.env.WEBHOOK_SECRET, sig)) {
      return res.status(401).send("Invalid signature");
    }

    const event = JSON.parse(req.body.toString());
    res.status(200).send("OK");
  }
);
Do not use express.json() on the webhook route. The middleware parses the body before you can verify the raw bytes.