Skip to main content

Webhook Security

When Fileloom sends webhooks to your endpoint, you should verify the signature to ensure the request is authentic and hasn’t been tampered with.

How Webhook Signing Works

Every webhook request includes a signature in the X-Fileloom-Signature header. This signature is created using:
  1. Your webhook secret (generated when you create the webhook)
  2. The raw request body
  3. HMAC-SHA256 algorithm
1

Fileloom Creates Payload

When an event occurs (e.g., PDF generated), Fileloom creates a JSON payload with the event data.
2

Fileloom Signs the Payload

Using your webhook secret, Fileloom computes an HMAC-SHA256 signature of the payload.
3

Your Server Receives the Request

The webhook is sent to your endpoint with the signature in the X-Fileloom-Signature header.
4

Your Server Verifies the Signature

Using the same secret, your server computes the expected signature and compares it.
5

Process if Valid

If signatures match, the request is authentic. Process the event data.

Webhook Headers

Each webhook request includes these headers:
HeaderDescription
X-Fileloom-SignatureHMAC-SHA256 signature
X-Fileloom-TimestampUnix timestamp when sent
X-Fileloom-EventEvent type (e.g., pdf.generated)
X-Fileloom-Delivery-IdUnique delivery ID

Verifying Signatures

Step 1: Get the Signature

Extract the signature from the X-Fileloom-Signature header:
X-Fileloom-Signature: sha256=a1b2c3d4e5f6...

Step 2: Compute Expected Signature

Using your webhook secret, compute the HMAC-SHA256 of the raw request body.

Step 3: Compare Signatures

Use a timing-safe comparison to check if the signatures match.

Code Examples

const crypto = require('crypto');

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

// Express.js example
app.post('/webhooks/fileloom', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.headers['x-fileloom-signature'];
  const payload = req.body.toString();
  
  if (!verifyWebhookSignature(payload, signature, process.env.WEBHOOK_SECRET)) {
    return res.status(401).send('Invalid signature');
  }
  
  const event = JSON.parse(payload);
  
  // Process the event
  switch (event.type) {
    case 'pdf.generated':
      console.log('PDF generated:', event.data.fileId);
      break;
    case 'pdf.failed':
      console.log('PDF failed:', event.data.error);
      break;
  }
  
  res.status(200).send('OK');
});

Preventing Replay Attacks

Use the X-Fileloom-Timestamp header to prevent replay attacks:
function verifyWebhook(payload, signature, timestamp, secret) {
  // Reject if timestamp is more than 5 minutes old
  const fiveMinutesAgo = Math.floor(Date.now() / 1000) - 300;
  if (parseInt(timestamp) < fiveMinutesAgo) {
    return false;
  }
  
  // Verify signature
  return verifyWebhookSignature(payload, signature, secret);
}

Webhook Secret Rotation

If your webhook secret is compromised:
1

Generate New Secret

In your dashboard, go to the webhook settings and regenerate the secret.
2

Update Your Server

Deploy the new secret to your webhook handler.
3

Test the Webhook

Use the “Send Test” button to verify it works.
Regenerating the secret immediately invalidates the old one. Update your server quickly to avoid missing events.

Troubleshooting

Common causes:
  • Wrong secret - Ensure you’re using the correct webhook secret
  • Payload modification - Don’t parse or modify the body before verifying
  • Encoding issues - Use raw bytes, not decoded JSON
  • Extra whitespace - Some frameworks add whitespace
Check that:
  • Your endpoint is receiving POST requests
  • The X-Fileloom-Signature header isn’t being stripped by a proxy
  • You’re reading the correct header name (case-insensitive)
Always use timing-safe comparison functions:
  • Node.js: crypto.timingSafeEqual()
  • Python: hmac.compare_digest()
  • PHP: hash_equals()
  • Go: hmac.Equal()
  • Ruby: Rack::Utils.secure_compare()
Regular string comparison (===) is vulnerable to timing attacks.

Security Checklist

  • Verify signature on every webhook request
  • Use timing-safe comparison
  • Check timestamp to prevent replay attacks
  • Store webhook secret in environment variables
  • Use HTTPS for your webhook endpoint
  • Return 200 quickly, process async if needed
  • Log failed verification attempts

Next Steps