SIGNATURE VERIFICATION
Verify before you trust.
The Fotowall-Signature header looks like t=1715000000,v1=ab12....
Concatenate the timestamp + "." + raw body, HMAC-SHA256 with your event's webhook secret,
compare in constant time, and reject if the timestamp is older than 5 minutes.
Node.js (Express)
import crypto from 'node:crypto';
const SECRET = process.env.FOTOWALL_WEBHOOK_SECRET!;
app.post('/webhooks/fotowall', express.raw({ type: 'application/json' }), (req, res) => {
const header = req.header('Fotowall-Signature') ?? '';
const parts = Object.fromEntries(header.split(',').map(p => p.split('=')));
const t = parts.t;
const v1 = parts.v1;
if (!t || !v1) return res.status(400).send('missing signature');
// Replay defense — reject anything older than 5 minutes.
if (Math.abs(Date.now() / 1000 - Number(t)) > 300) {
return res.status(400).send('stale');
}
const signed = `${t}.${req.body.toString('utf8')}`;
const expected = crypto.createHmac('sha256', SECRET).update(signed).digest('hex');
// Constant-time comparison.
const a = Buffer.from(expected, 'hex');
const b = Buffer.from(v1, 'hex');
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
return res.status(401).send('bad signature');
}
const payload = JSON.parse(req.body.toString('utf8'));
// ... process payload, return 200 fast, do work async ...
res.status(200).end();
});
Python (Flask)
import hmac, hashlib, time, os
from flask import request, abort
SECRET = os.environ['FOTOWALL_WEBHOOK_SECRET'].encode()
@app.post('/webhooks/fotowall')
def fotowall():
header = request.headers.get('Fotowall-Signature', '')
parts = dict(p.split('=', 1) for p in header.split(','))
t, v1 = parts.get('t'), parts.get('v1')
if not t or not v1:
abort(400)
if abs(time.time() - int(t)) > 300:
abort(400)
signed = f"{t}.{request.get_data(as_text=True)}".encode()
expected = hmac.new(SECRET, signed, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, v1):
abort(401)
payload = request.get_json(force=True)
# ... handle, return 200, queue work async ...
return '', 200