Verifying signatures
Every webhook delivery includes the X-ParaSta-Signature header:
X-ParaSta-Signature: t=1730000000,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bdtis the Unix timestamp (seconds) when ParaSta signed the request.v1is the HMAC-SHA256 hex digest of<t>.<raw body>, keyed with your endpoint’s signing secret.
Verification algorithm
Section titled “Verification algorithm”- Parse
tandv1from the header. - Reject if
tis older than 5 minutes (or in the future by more than 5 minutes). - Compute
expected = HMAC_SHA256(secret, t + '.' + raw_body)(hex). - Compare
expectedtov1using constant-time comparison.
import crypto from 'node:crypto';
function verify(rawBody, header, secret) { const parts = Object.fromEntries(header.split(',').map(s => s.split('='))); const t = parts.t, v1 = parts.v1; if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false; const expected = crypto.createHmac('sha256', secret).update(t + '.' + rawBody).digest('hex'); return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1));}
// Express:app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => { const ok = verify(req.body.toString('utf8'), req.headers['x-parasta-signature'], process.env.WEBHOOK_SECRET); if (!ok) return res.status(400).send('bad signature'); const event = JSON.parse(req.body.toString('utf8')); // ...handle event res.json({ received: true });});import hmac, hashlib, time
def verify(raw_body: bytes, header: str, secret: str) -> bool: parts = dict(p.split('=', 1) for p in header.split(',')) t, v1 = parts['t'], parts['v1'] if abs(int(time.time()) - int(t)) > 300: return False expected = hmac.new(secret.encode(), f'{t}.'.encode() + raw_body, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, v1)
# Flask:@app.post('/webhook')def webhook(): raw = request.get_data() if not verify(raw, request.headers['X-ParaSta-Signature'], os.environ['WEBHOOK_SECRET']): abort(400) event = json.loads(raw) # ...handle event return {'received': True}# (Verification is server-side; cURL example for manual testing only)# Recompute on the command line:body='{"id":"evt_test"}'ts=1730000000signature=$(printf "%s.%s" "$ts" "$body" | openssl dgst -sha256 -hmac "$WEBHOOK_SECRET" | awk '{print $2}')echo "t=$ts,v1=$signature"Common mistakes
Section titled “Common mistakes”- Parsing the body before verifying — the JSON parse changes whitespace, breaking the HMAC. Always verify on the raw bytes of the body.
- Reusing a single global signing secret across endpoints — each endpoint has its own secret.
- Using string equality instead of constant-time compare — opens timing attacks.