Skip to main content

Webhook Signatures

When you configure a webhook secret for your inbound domain, every webhook request is signed so you can verify it came from JetEmail and hasn’t been tampered with.

Signature Headers

Each signed webhook request includes these headers:
HeaderDescription
X-Webhook-IDUnique job identifier
X-Webhook-TimestampUnix timestamp of the delivery attempt
X-Webhook-SignatureHMAC-SHA256 hex-encoded signature

How the Signature is Computed

The signature is computed using HMAC-SHA256 with your webhook secret. The signing input combines the webhook ID, timestamp, and request body:
{webhook_id}.{timestamp}.{json_body}
The resulting hex-encoded digest is sent in the X-Webhook-Signature header.

Verifying Signatures

const crypto = require('crypto');

function verifyWebhookSignature(webhookId, timestamp, body, signature, secret) {
  const signingInput = `${webhookId}.${timestamp}.${body}`;
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(signingInput)
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expectedSignature)
  );
}

// Express.js example
app.post('/inbound', express.raw({ type: 'application/json' }), (req, res) => {
  const webhookId = req.headers['x-webhook-id'];
  const timestamp = req.headers['x-webhook-timestamp'];
  const signature = req.headers['x-webhook-signature'];
  const body = req.body.toString();

  if (!verifyWebhookSignature(webhookId, timestamp, body, signature, process.env.WEBHOOK_SECRET)) {
    return res.status(401).send('Invalid signature');
  }

  const email = JSON.parse(body);
  // Process the inbound email...

  res.status(200).send('OK');
});

Security Best Practices

Always Verify Signatures

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

Use HTTPS

Always use HTTPS for your webhook endpoint to ensure email content is encrypted in transit.

Use Timing-Safe Comparison

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

Validate the Timestamp

Check that the X-Webhook-Timestamp is recent (within 5 minutes) to prevent replay attacks.

Replay Prevention

To prevent replay attacks, 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 and retry delays.