Skip to content

Webhook Security

Securing your webhook endpoints is critical. This guide covers best practices for verifying and protecting webhook deliveries.

Every webhook includes a signature in the X-LMIF-Signature header. Always verify this signature.

X-LMIF-Signature: sha256=abc123...

The signature is an HMAC-SHA256 hash of the raw request body using your webhook secret.

import { LMIFClient } from '@lookmaimfamous/lmif';
const lmif = new LMIFClient({ apiKey: process.env.LMIF_API_KEY });
app.post('/webhooks/lmif', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-lmif-signature'];
const payload = req.body; // Raw buffer
const isValid = lmif.webhooks.verify(
payload,
signature,
process.env.LMIF_WEBHOOK_SECRET
);
if (!isValid) {
return res.status(401).send('Invalid signature');
}
// Process webhook...
});

Webhooks include a timestamp to prevent replay attacks:

{
"id": "evt_abc123",
"type": "box.created",
"createdAt": "2024-01-15T10:00:00Z",
...
}

Reject webhooks older than 5 minutes:

function validateTimestamp(event: WebhookEvent): boolean {
const eventTime = new Date(event.createdAt).getTime();
const now = Date.now();
const fiveMinutes = 5 * 60 * 1000;
return now - eventTime < fiveMinutes;
}
app.post('/webhooks/lmif', (req, res) => {
// After signature verification...
const event = JSON.parse(req.body);
if (!validateTimestamp(event)) {
console.warn('Webhook timestamp too old:', event.id);
return res.status(400).send('Webhook expired');
}
// Process webhook...
});

Webhooks may be delivered multiple times. Handle duplicates safely:

// Use a cache or database to track processed events
const processedEvents = new Set<string>();
async function handleWebhook(event: WebhookEvent) {
// Check if already processed
if (processedEvents.has(event.id)) {
console.log('Duplicate webhook, skipping:', event.id);
return;
}
// For production, use a database
const existing = await db.webhookEvents.findOne({ eventId: event.id });
if (existing) {
return;
}
// Process the event
await processEvent(event);
// Mark as processed
await db.webhookEvents.insert({
eventId: event.id,
processedAt: new Date()
});
processedEvents.add(event.id);
}
Terminal window
# .env (never commit this file)
LMIF_WEBHOOK_SECRET=whsec_abc123...

Rotate your webhook secret periodically:

// Rotate the secret
const newSecret = await lmif.webhooks.rotateSecret('wh_xyz789');
// You have 24 hours to update your code
// Both old and new secrets work during this period
console.log('New secret:', newSecret);

For additional security, allowlist LMIF’s IP addresses:

EnvironmentIP Addresses
ProductionContact support for current IPs
SandboxContact support for current IPs
const ALLOWED_IPS = ['203.0.113.1', '203.0.113.2']; // Example IPs
app.post('/webhooks/lmif', (req, res) => {
const clientIp = req.ip || req.headers['x-forwarded-for'];
if (!ALLOWED_IPS.includes(clientIp)) {
console.warn('Webhook from unknown IP:', clientIp);
return res.status(403).send('Forbidden');
}
// Process webhook...
});

Webhook endpoints must use HTTPS in production:

  • https://yoursite.com/webhooks/lmif
  • http://yoursite.com/webhooks/lmif

Don’t expose internal errors in responses:

app.post('/webhooks/lmif', async (req, res) => {
try {
// Verify and process...
await processWebhook(req.body);
res.status(200).send('OK');
} catch (error) {
// Log the full error internally
console.error('Webhook processing error:', error);
// Return generic error to client
res.status(500).send('Internal error');
}
});
  • Verify signatures on every webhook
  • Use timing-safe comparison for signatures
  • Validate timestamps to prevent replay attacks
  • Handle duplicate deliveries idempotently
  • Store secrets in environment variables
  • Rotate secrets regularly
  • Use HTTPS in production
  • Consider IP allowlisting
  • Don’t expose internal errors
  • Log all verification failures