When you configure a signing secret for your webhook, Catalogian includes an HMAC-SHA256 signature in the X-Catalogian-Signature header. Always verify this signature before processing the payload.
Catalogian computes the signature by hashing the raw JSON request body with your secret:
signature = "sha256=" + HMAC-SHA256(secret, rawBody).hexDigest()
The signature is sent in the X-Catalogian-Signature header. Your server should compute the same hash and compare using a timing-safe comparison function.
const crypto = require("crypto");
function verifySignature(secret, rawBody, signature) {
const expected =
"sha256=" +
crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signature)
);
}
// In your Express/Fastify handler:
app.post("/webhooks/catalogian", (req, res) => {
const signature = req.headers["x-catalogian-signature"];
const rawBody = req.rawBody; // Ensure your framework preserves raw body
if (!signature || !verifySignature(WEBHOOK_SECRET, rawBody, signature)) {
return res.status(401).send("Invalid signature");
}
// Signature valid — process the payload
const payload = JSON.parse(rawBody);
res.status(200).send("OK");
});Use the raw body, not parsed JSON.The signature is computed over the exact bytes sent by Catalogian. If your framework parses and re-serializes the JSON, the whitespace may differ and the signature won't match.
import hmac
import hashlib
def verify_signature(secret: str, raw_body: bytes, signature: str) -> bool:
expected = "sha256=" + hmac.new(
secret.encode(),
raw_body,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)
# In a Flask handler:
@app.route("/webhooks/catalogian", methods=["POST"])
def handle_webhook():
signature = request.headers.get("X-Catalogian-Signature", "")
if not verify_signature(WEBHOOK_SECRET, request.data, signature):
return "Invalid signature", 401
payload = request.json
# Process the payload...
return "OK", 200require "openssl"
def verify_signature(secret, raw_body, signature)
expected = "sha256=" + OpenSSL::HMAC.hexdigest(
"SHA256", secret, raw_body
)
Rack::Utils.secure_compare(expected, signature)
end
# In a Sinatra/Rails controller:
post "/webhooks/catalogian" do
raw_body = request.body.read
signature = request.env["HTTP_X_CATALOGIAN_SIGNATURE"]
halt 401, "Invalid signature" unless verify_signature(
ENV["WEBHOOK_SECRET"], raw_body, signature
)
payload = JSON.parse(raw_body)
# Process the payload...
status 200
endWhen creating or updating a webhook, provide a secret value:
curl -X POST https://api.catalogian.com/v1/sources/:id/webhooks \
-H "Authorization: Bearer $CATALOGIAN_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-app.com/webhooks/catalogian",
"secret": "whsec_your_signing_secret_here"
}'You can also set the secret in the dashboard under Source → Webhooks → Edit.
Generate a test signature to verify your implementation:
# Generate a test signature
echo -n '{"event":"delta","sourceId":"test"}' | \
openssl dgst -sha256 -hmac "your_secret" | \
awk '{print "sha256="$2}'Understand retry behavior and failure handling. Retry Logic →