Skip to main content

Webhook Security

Securing your webhook endpoints is crucial to ensure incoming requests are legitimate and come from DeePsy. This guide covers HMAC signature verification and security best practices.

Security First

Always implement signature verification for production webhooks. Without it, anyone who knows your webhook URL can send fake events to your application.

HMAC Signature Verification

DeePsy signs webhook payloads using HMAC-SHA256 to ensure authenticity. When signature verification is enabled, each webhook request includes a signature header you can use to verify the payload hasn't been tampered with.

How It Works

  1. Secret Generation: When you enable signatures, DeePsy generates a unique secret for your webhook (prefixed with whsec_)
  2. Signature Creation: For each webhook, DeePsy creates an HMAC-SHA256 hash of the JSON payload using your secret
  3. Header Inclusion: The signature is sent in the X-Webhook-Signature header
  4. Verification: Your application recreates the hash and compares it with the received signature

Enabling Signatures

Enable signature verification when creating a webhook in the dashboard or set signature_enabled: true when using the API.

Secret Storage

The webhook secret is shown only once during creation. Store it securely immediately - you cannot retrieve it later. If you lose it, you'll need to regenerate a new secret.

Signature Header Format

Signed webhook requests include the X-Webhook-Signature header:

X-Webhook-Signature: sha256=<hmac_signature>

Implementation Examples

Here's how to verify webhook signatures in different languages:

const crypto = require('crypto');

function verifyWebhookSignature(payload, signature, secret) {
if (!signature || !signature.startsWith('sha256=')) {
return false;
}

const expectedSignature = signature.substring(7); // Remove 'sha256=' prefix
const computedSignature = crypto
.createHmac('sha256', secret)
.update(payload, 'utf8')
.digest('hex');

// Use timing-safe comparison to prevent timing attacks
return crypto.timingSafeEqual(
Buffer.from(expectedSignature, 'hex'),
Buffer.from(computedSignature, 'hex')
);
}

// Usage in Express (important: use raw body parser)
const express = require('express');
const app = express();

app.post('/webhooks/deepsy',
express.raw({type: 'application/json'}),
(req, res) => {
const signature = req.headers['x-webhook-signature'];
const payload = req.body; // Raw buffer, not parsed JSON
const secret = process.env.WEBHOOK_SECRET; // whsec_...

if (!verifyWebhookSignature(payload, signature, secret)) {
return res.status(401).send('Unauthorized');
}

// Now it's safe to parse the JSON
const event = JSON.parse(payload);
console.log('Verified event:', event.event);

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

Security Best Practices

Store Secrets Securely

Never hardcode webhook secrets in your application:

// ✅ Good - Use environment variables
const secret = process.env.WEBHOOK_SECRET;

// ❌ Bad - Hardcoded secret
const secret = 'whsec_abc123...';

Rotate Secrets Regularly

Regenerate webhook secrets periodically through the dashboard or API:

# Regenerate secret via API
curl -X POST "https://deepsy.fr/api/v1/webhooks/{webhook_id}/regenerate-secret" \
-H "Authorization: Bearer deepsy-dev-your-api-key-here"

Use Different Secrets for Different Environments

Use separate webhook secrets for development, staging, and production environments.

Endpoint Security

Use HTTPS Only

All webhook URLs must use HTTPS to ensure data transmission security:

// ✅ Good
const webhookUrl = 'https://your-domain.com/webhooks/deepsy';

// ❌ Bad - HTTP not allowed
const webhookUrl = 'http://your-domain.com/webhooks/deepsy';

Validate Content-Type

Ensure incoming requests have the correct content type:

app.post('/webhooks/deepsy', (req, res) => {
if (req.headers['content-type'] !== 'application/json') {
return res.status(400).send('Invalid content type');
}
// Process webhook...
});

Implement Rate Limiting

Protect your endpoint from abuse:

const rateLimit = require('express-rate-limit');

const webhookLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // Limit each IP to 100 requests per windowMs
message: 'Too many webhook requests'
});

app.post('/webhooks/deepsy', webhookLimiter, (req, res) => {
// Process webhook...
});

Request Validation

Beyond signature verification, implement additional validation:

function validateWebhookPayload(event) {
// Validate required fields
if (!event.event || !event.webhook_id || !event.company_id) {
throw new Error('Missing required fields');
}

// Validate event type
const validEvents = [
'email.sent', 'email.delivered', 'email.bounced', 'email.dropped', 'email.opened', 'email.clicked',
'campaign.created', 'campaign.updated', 'campaign.archived',
'candidate.created', 'candidate.questionnaire_completed', 'candidate.test_started',
'candidate.test_completed', 'candidate.evaluation_completed',
'user.created', 'user.invited'
];

if (!validEvents.includes(event.event)) {
throw new Error('Invalid event type');
}

// Validate timestamp (reject old events)
const eventTime = new Date(event.timestamp);
const now = new Date();
const maxAge = 5 * 60 * 1000; // 5 minutes

if (now - eventTime > maxAge) {
throw new Error('Event too old');
}

return true;
}

Error Handling

Handle errors gracefully while maintaining security:

app.post('/webhooks/deepsy', (req, res) => {
try {
// Verify signature
if (!verifyWebhookSignature(req.body, req.headers['x-webhook-signature'], secret)) {
return res.status(401).send('Unauthorized');
}

// Validate payload
const event = JSON.parse(req.body);
validateWebhookPayload(event);

// Process event
processWebhookEvent(event);

res.status(200).send('OK');
} catch (error) {
// Log error for debugging but don't expose details
console.error('Webhook processing error:', error);

// Return generic error to avoid information leakage
res.status(400).send('Bad Request');
}
});

Idempotency

Handle duplicate events by implementing idempotency:

const processedEvents = new Set();

function processWebhookEvent(event) {
const eventId = `${event.webhook_id}-${event.timestamp}-${event.event}`;

if (processedEvents.has(eventId)) {
console.log('Duplicate event ignored:', eventId);
return;
}

processedEvents.add(eventId);

// Process the event
// ...

// Clean up old event IDs periodically
if (processedEvents.size > 10000) {
processedEvents.clear();
}
}

Troubleshooting

For signature verification issues and other security-related problems, see our Troubleshooting Guide.

Next Steps

Now that you understand webhook security:

Need Help?

For additional security questions: