Skip to content

Verifying signatures

Every webhook delivery includes the X-ParaSta-Signature header:

X-ParaSta-Signature: t=1730000000,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
  • t is the Unix timestamp (seconds) when ParaSta signed the request.
  • v1 is the HMAC-SHA256 hex digest of <t>.<raw body>, keyed with your endpoint’s signing secret.
  1. Parse t and v1 from the header.
  2. Reject if t is older than 5 minutes (or in the future by more than 5 minutes).
  3. Compute expected = HMAC_SHA256(secret, t + '.' + raw_body) (hex).
  4. Compare expected to v1 using 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 });
});
  • 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.