Skip to main content

Webhook Security Best Practices

Securing your webhook endpoints is critical to prevent unauthorized access and ensure data integrity.

Security Checklist​

Use this checklist to ensure your webhook implementation is secure:

#PracticePriorityStatus
1Use HTTPS endpoints only🔴 Critical☐
2Verify webhook secret token🔴 Critical☐
3Validate payload structure🟡 High☐
4Implement rate limiting🟡 High☐
5Handle duplicates (idempotency)🟡 High☐
6Log all requests🟢 Medium☐
7Set up monitoring/alerts🟢 Medium☐

Authentication Methods​

Secret Token Verification​

Required

Always verify webhook authenticity using a secret token header.

Step 1: Configure secret header in Pollarix

Header KeyValue
X-Webhook-Secretyour-random-secret-here

Step 2: Generate a secure secret

Generate secure token
openssl rand -hex 32
# Output: a1b2c3d4e5f6789...

Step 3: Verify in your handler

Secure webhook handler
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET;

app.post('/webhooks/pollarix', (req, res) => {
const receivedSecret = req.headers['x-webhook-secret'];

if (receivedSecret !== WEBHOOK_SECRET) {
console.warn('⚠️ Invalid webhook secret!', {
ip: req.ip,
timestamp: new Date().toISOString()
});
return res.status(401).send('Unauthorized');
}

// Secret verified - process webhook
processWebhook(req.body);
res.status(200).send('OK');
});

Bearer Token Authentication​

Use standard Bearer token authentication:

HeaderValue
AuthorizationBearer your-api-token
Bearer token verification
app.post('/webhooks/pollarix', (req, res) => {
const authHeader = req.headers['authorization'];
const expectedToken = `Bearer ${process.env.WEBHOOK_TOKEN}`;

if (authHeader !== expectedToken) {
return res.status(401).send('Unauthorized');
}

// Process webhook...
res.status(200).send('OK');
});

HTTPS Requirements​

Never Use HTTP

Plain HTTP exposes your webhook data to:

  • Man-in-the-middle attacks - Data can be intercepted
  • Spoofing - Attackers can impersonate Pollarix
  • Data leakage - Survey responses exposed

Free SSL Options​

ProviderTypeEffort
Let's EncryptFree automatedLow
CloudflareFree proxy SSLLow
AWS ACMFree (with AWS)Medium

Payload Validation​

Always validate incoming payloads before processing:

Payload validation with Joi
const Joi = require('joi');

const webhookSchema = Joi.object({
event: Joi.string().valid(
'SURVEY_COMPLETED',
'SURVEY_STATUS_CHANGED',
'COUPON_REDEEMED'
).required(),
timestamp: Joi.string().isoDate().required(),
webhookId: Joi.string().required(),
departmentId: Joi.string().required(),
data: Joi.object().required()
});

app.post('/webhooks/pollarix', (req, res) => {
const { error, value } = webhookSchema.validate(req.body);

if (error) {
console.error('Invalid payload:', error.message);
return res.status(400).send('Invalid payload');
}

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

Rate Limiting​

Protect your endpoint from abuse:

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

const webhookLimiter = rateLimit({
windowMs: 60 * 1000, // 1 minute
max: 100, // 100 requests per minute
message: 'Too many webhook requests',
handler: (req, res) => {
console.warn('Rate limit exceeded:', req.ip);
res.status(429).send('Too many requests');
}
});

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

Idempotency​

Handle duplicate webhook deliveries safely:

Idempotent webhook handler
const processedEvents = new Map();
const EVENT_TTL = 24 * 60 * 60 * 1000; // 24 hours

function cleanupOldEvents() {
const now = Date.now();
for (const [key, timestamp] of processedEvents) {
if (now - timestamp > EVENT_TTL) {
processedEvents.delete(key);
}
}
}

// Run cleanup hourly
setInterval(cleanupOldEvents, 60 * 60 * 1000);

app.post('/webhooks/pollarix', (req, res) => {
const { webhookId, timestamp } = req.body;
const eventKey = `${webhookId}-${timestamp}`;

if (processedEvents.has(eventKey)) {
return res.status(200).send('Already processed');
}

processedEvents.set(eventKey, Date.now());

// Process webhook...
res.status(200).send('OK');
});

Common Security Mistakes​

DON'T do this
// No secret verification
app.post('/webhooks', (req, res) => {
// Trusting any request 😱
processWebhook(req.body);
res.send('OK');
});

Logging and Monitoring​

Log All Webhook Requests​

Structured logging
const winston = require('winston');

const logger = winston.createLogger({
level: 'info',
format: winston.format.json(),
transports: [
new winston.transports.File({ filename: 'webhooks.log' })
]
});

app.post('/webhooks/pollarix', (req, res) => {
logger.info('Webhook received', {
event: req.body.event,
webhookId: req.body.webhookId,
timestamp: req.body.timestamp,
ip: req.ip,
userAgent: req.headers['user-agent']
});

// Process webhook...
});

Monitor for Anomalies​

Set up alerts for:

  • Unusual spike in webhook volume
  • High error rates
  • Requests from unexpected IPs
  • Invalid authentication attempts

Error Handling​

Don't Expose Internal Errors​

Safe error handling
app.post('/webhooks/pollarix', async (req, res) => {
try {
await processWebhook(req.body);
res.status(200).send('OK');
} catch (error) {
// Log detailed error internally
console.error('Webhook processing failed:', error);

// Return generic error to client
res.status(500).send('Internal error');
}
});

Graceful Degradation​

Queue for retry on failure
app.post('/webhooks/pollarix', async (req, res) => {
// Always acknowledge receipt quickly
res.status(200).send('OK');

try {
await processWebhook(req.body);
} catch (error) {
// Queue for retry
await webhookQueue.add('retry', {
payload: req.body,
attempts: 0
});
}
});

Summary​

PracticePriorityDifficulty
Use HTTPS🔴 CriticalLow
Token authentication🔴 CriticalLow
Payload validation🟡 HighMedium
Idempotency handling🟡 HighMedium
Rate limiting🟢 MediumLow
Logging & monitoring🟢 MediumLow

Next Steps​

Was this page helpful?