Skip to main content

Webhook Signatures

Every webhook request includes a signature that you should verify to ensure the request came from JetEmail and hasn’t been tampered with.

Signature Headers

Each webhook request includes these headers:
HeaderDescription
X-Webhook-IDUnique identifier for this event
X-Webhook-TimestampUnix timestamp when the event was sent
X-Webhook-SignatureHMAC-SHA256 signature (format: sha256=...)

Verifying Signatures

The signature is computed using HMAC-SHA256 with your webhook secret and the raw request body.
const crypto = require('crypto');

function verifyWebhookSignature(payload, signature, secret) {
  const expectedSignature = 'sha256=' + crypto
    .createHmac('sha256', secret)
    .update(payload)
    .digest('hex');
  
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expectedSignature)
  );
}

// Express.js example
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.headers['x-webhook-signature'];
  const payload = req.body;
  
  if (!verifyWebhookSignature(payload, signature, process.env.WEBHOOK_SECRET)) {
    return res.status(401).send('Invalid signature');
  }
  
  const event = JSON.parse(payload);
  // Process the event...
  
  res.status(200).send('OK');
});

Security Best Practices

Always Verify Signatures

Never process webhook events without verifying the signature first. This prevents attackers from sending fake events to your endpoint.

Use HTTPS

Always use HTTPS for your webhook endpoint to ensure the payload is encrypted in transit.

Use Timing-Safe Comparison

Use constant-time string comparison functions to prevent timing attacks when verifying signatures.

Handle Duplicate Events

Use the X-Webhook-ID header to detect and handle duplicate events. Store processed event IDs and skip duplicates.

Replay Prevention

To prevent replay attacks, you can verify that the timestamp is recent:
function isValidTimestamp(timestamp, toleranceSeconds = 300) {
  const now = Math.floor(Date.now() / 1000);
  const diff = Math.abs(now - timestamp);
  return diff <= toleranceSeconds;
}

// Usage
const timestamp = parseInt(req.headers['x-webhook-timestamp']);
if (!isValidTimestamp(timestamp)) {
  return res.status(401).send('Request too old');
}
A tolerance of 5 minutes (300 seconds) is recommended to account for clock drift between servers.

Rotating Secrets

If you need to rotate your webhook secret:
  1. Generate a new secret in Dashboard → Webhooks
  2. Update your application to accept both the old and new secrets
  3. Once all events are using the new secret, remove the old one from your code