Request Signature

Every webhook request from Emailit includes an X-Emailit-Signature header containing an HMAC-SHA256 signature and an X-Emailit-Timestamp header with the Unix timestamp of when the request was signed. You should verify this signature to ensure the request is authentic and has not been tampered with.

How it works

  1. When you create a webhook, Emailit generates a signing secret for that endpoint
  2. For each delivery, Emailit concatenates the timestamp and raw request body as {timestamp}.{rawBody}
  3. An HMAC-SHA256 hash is computed over that string using your signing secret
  4. The signature is sent in the X-Emailit-Signature header and the timestamp in X-Emailit-Timestamp
  5. Your server recomputes the signature and compares it to the header value

Verifying the signature

To verify a webhook signature:

  1. Extract the X-Emailit-Signature and X-Emailit-Timestamp headers from the request
  2. Concatenate the timestamp and the raw request body as {timestamp}.{rawBody}
  3. Compute an HMAC-SHA256 hash of that string using your webhook signing secret
  4. Compare the computed hash with the signature using a timing-safe comparison
  5. Optionally, reject requests where the timestamp is older than a few minutes to guard against replay attacks

Verify signature

import crypto from 'crypto';

function verifyWebhookSignature(rawBody, signature, timestamp, secret) {
  const signedPayload = `${timestamp}.${rawBody}`;
  const computed = crypto
    .createHmac('sha256', secret)
    .update(signedPayload, 'utf8')
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(computed, 'hex'),
    Buffer.from(signature, 'hex')
  );
}

// In your webhook handler:
const rawBody = req.body; // raw request body as string
const signature = req.headers['x-emailit-signature'];
const timestamp = req.headers['x-emailit-timestamp'];
const secret = process.env.WEBHOOK_SIGNING_SECRET;

// Guard against replay attacks (5 minute tolerance)
const age = Math.floor(Date.now() / 1000) - parseInt(timestamp, 10);
if (age > 300) {
  return res.status(401).send('Request too old');
}

if (!verifyWebhookSignature(rawBody, signature, timestamp, secret)) {
  return res.status(401).send('Invalid signature');
}

Important notes

  • The signature is computed over {timestamp}.{rawBody} — always use the raw request body string, not a re-serialized version (e.g. JSON.stringify(req.body)). Whitespace or key ordering differences will break the signature.
  • Always use a timing-safe comparison function (e.g. crypto.timingSafeEqual, hmac.compare_digest, hash_equals) to prevent timing attacks.
  • Consider rejecting requests where the X-Emailit-Timestamp is older than a few minutes to guard against replay attacks.
  • Keep your signing secret secure — store it in environment variables, not in source code.
  • You can find your webhook signing secret in the Emailit dashboard under the webhook settings, or via the Webhooks API.