Every webhook delivery includes an X-Signature header so you can confirm it came from Etherfuse and wasn’t tampered with.
How It Works
When you create a webhook, the response includes a secret — a base64-encoded HMAC key. Store it securely; it is only returned once.
Each delivery signs the payload with that secret:
- The JSON body is canonicalized (RFC 8785 JCS — deterministic key ordering, no extra whitespace)
- HMAC-SHA256 is computed over the canonicalized string using your secret (decoded from base64)
- The result is sent as
X-Signature: sha256={hex}
Verifying the Signature
- Canonicalize the received JSON body
- Decode your webhook secret from base64
- Compute HMAC-SHA256 over the canonicalized string
- Compare
sha256={hex_result} to the X-Signature header using a constant-time comparison
The signature is computed over the canonicalized JSON, not the raw request body. You must canonicalize before comparing or the signature will not match.
Node.js
import { createHmac, timingSafeEqual } from "crypto";
import canonicalize from "canonicalize"; // npm install canonicalize
function verifyWebhook(body, secret, signatureHeader) {
const canonicalized = canonicalize(body);
const key = Buffer.from(secret, "base64");
const hmac = createHmac("sha256", key).update(canonicalized).digest("hex");
const expected = `sha256=${hmac}`;
if (expected.length !== signatureHeader.length) return false;
return timingSafeEqual(Buffer.from(expected), Buffer.from(signatureHeader));
}
// Usage in an Express handler:
app.post("/webhook", (req, res) => {
const signature = req.headers["x-signature"];
if (!verifyWebhook(req.body, process.env.WEBHOOK_SECRET, signature)) {
return res.status(401).send("Invalid signature");
}
// Process the event...
res.sendStatus(200);
});
Python
import hmac, hashlib, base64
from canonicaljson import encode_canonical_json # pip install canonicaljson
def verify_webhook(body: dict, secret: str, signature_header: str) -> bool:
canonicalized = encode_canonical_json(body)
key = base64.b64decode(secret)
digest = hmac.new(key, canonicalized, hashlib.sha256).hexdigest()
expected = f"sha256={digest}"
return hmac.compare_digest(expected, signature_header)
Delivery & Retries
Failed deliveries (non-2xx responses or connection errors) are retried up to 3 times with 5-second delays between attempts. Return a 2xx promptly to avoid unnecessary retries.