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?