Trasys
Webhooks

Security & Verification

Verify incoming Trasys webhook requests are authentic using HMAC-SHA256 signatures and timestamp headers, in the same format Stripe webhooks already use.

Every webhook request Trasys sends includes two extra headers:

X-Trasys-Signature: sha256=<hmac-hex>
X-Trasys-Timestamp: <unix-seconds>

How signatures work

  1. Trasys builds a signed string: {timestamp}.{raw-body}
  2. It computes HMAC-SHA256(secret, signed-string)
  3. The hex digest is sent as X-Trasys-Signature: sha256=<hex>

This is the same format as Stripe webhooks, so existing middleware often works directly.

Verifying in Node.js

import { createHmac, timingSafeEqual } from 'node:crypto';

function verifyWebhook(rawBody, headers, secret) {
  const signature = headers['x-trasys-signature'];
  const timestamp  = headers['x-trasys-timestamp'];

  if (!signature || !timestamp) return false;

  // Reject stale events (older than 5 minutes)
  const age = Math.abs(Date.now() / 1000 - Number(timestamp));
  if (age > 300) return false;

  const expected = 'sha256=' + createHmac('sha256', secret)
    .update(`${timestamp}.${rawBody}`)
    .digest('hex');

  try {
    return timingSafeEqual(
      Buffer.from(signature),
      Buffer.from(expected)
    );
  } catch {
    return false;
  }
}

// Express example — use raw body parser!
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const valid = verifyWebhook(req.body, req.headers, process.env.WEBHOOK_SECRET);
  if (!valid) return res.status(401).json({ error: 'Invalid signature' });

  res.sendStatus(200);
  const event = JSON.parse(req.body);
  handleEvent(event);
});

Use express.raw() or equivalent, not express.json(). Signature verification requires the exact raw bytes of the body. Re-serialising parsed JSON can change whitespace and invalidate the HMAC.

Verifying in Python

import hashlib, hmac, time

def verify_webhook(raw_body: bytes, headers: dict, secret: str) -> bool:
    signature = headers.get("x-trasys-signature", "")
    timestamp  = headers.get("x-trasys-timestamp", "")

    if not signature or not timestamp:
        return False

    # Reject events older than 5 minutes
    if abs(time.time() - float(timestamp)) > 300:
        return False

    signed  = f"{timestamp}.{raw_body.decode()}"
    expected = "sha256=" + hmac.new(
        secret.encode(), signed.encode(), hashlib.sha256
    ).hexdigest()

    return hmac.compare_digest(signature, expected)

Replay protection

The X-Trasys-Timestamp header is a Unix timestamp (seconds). You should reject any event where the timestamp is more than 5 minutes in the past or future to protect against replay attacks.

Rotating your secret

Webhook secrets cannot be retrieved after creation — if you lose yours, delete the endpoint and recreate it. A new secret will be shown once.

HeaderFormatNotes
X-Trasys-Signaturesha256=<64-char-hex>Always verify this
X-Trasys-TimestampUnix seconds (string)Reject if `
X-Trasys-EventEvent type stringInformational only

On this page