1. Signature verification
WatchDog signs every outgoing webhook with HMAC-SHA256 over the raw request body, encoded as hex. Two headers are sent:
X-Polsia-Signature— the hex digest, computed over the exact bytes of the request body. Re-stringifying after a JSON.parse will not reproduce the signature.X-Polsia-Event— one ofincident.acknowledged</code>, <code>incident.resolved, so you can branch on event type before parsing the body.
Subscribe in your dashboard to receive a 64-character hex secret (32 random bytes). Treat it like a password: store it in your secret manager, never commit it, and use a constant-time comparison when validating.
Node (Express)
// Express — keep the body raw so the signature matches the bytes WatchDog signed.
// app.post('/webhooks/watchdog', express.raw({ type: 'application/json' }), (req, res) => {
// const rawBody = req.body; // Buffer, NOT a parsed object
// const expected = crypto
// .createHmac('sha256', process.env.WH_SECRET)
// .update(rawBody)
// .digest('hex');
// const received = req.header('X-Polsia-Signature') || '';
// const ok =
// expected.length === received.length &&
// crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(received));
// if (!ok) return res.status(401).send('bad signature');
// const event = req.header('X-Polsia-Event');
// const payload = JSON.parse(rawBody.toString('utf8'));
// // ... handle event + payload here ...
// res.status(200).send('ok');
// });
Python (Flask)
# Flask — keep the body raw so the signature matches the bytes WatchDog signed.
import hmac, hashlib
from flask import Flask, request, abort
app = Flask(__name__)
WH_SECRET = b"<your-64-char-hex-secret>"
@app.post("/webhooks/watchdog")
def watchdog():
raw_body = request.get_data() # bytes — do NOT json.loads first
expected = hmac.new(WH_SECRET, raw_body, hashlib.sha256).hexdigest()
received = request.headers.get("X-Polsia-Signature", "")
if not hmac.compare_digest(expected, received):
abort(401)
event = request.headers.get("X-Polsia-Event")
payload = request.get_json()
# ... handle event + payload here ...
return "ok", 200
Ruby (Sinatra)
# Sinatra — keep the body raw so the signature matches the bytes WatchDog signed.
require "openssl"
require "sinatra"
require "rack/utils"
WH_SECRET = "<your-64-char-hex-secret>"
post "/webhooks/watchdog" do
raw_body = request.body.read # string — do NOT JSON.parse first
expected = OpenSSL::HMAC.hexdigest("SHA256", WH_SECRET, raw_body)
received = request.env["HTTP_X_POLSIA_SIGNATURE"].to_s
unless Rack::Utils.secure_compare(expected, received)
halt 401, "bad signature"
end
event = request.env["HTTP_X_POLSIA_EVENT"]
payload = JSON.parse(raw_body)
# ... handle event + payload here ...
"ok"
end