Webhooks let external services notify your application of events in real time. Since they accept incoming requests from the internet, designing them securely is essential — this guide covers the key practices.
What Makes Webhook Endpoints Different from Regular API Endpoints
Unlike your normal API where you control the client, webhook endpoints must accept requests from a third party you don't directly control — making signature verification and validation especially important.
Step 1 — Always Verify Webhook Signatures
Most reputable services (Stripe, GitHub, etc.) sign webhook payloads with a shared secret. Verify this signature before processing anything:
const crypto = require('crypto');
function verifySignature(payload, signature, secret) {
const expected = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}
app.post('/webhooks/provider', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-provider-signature'];
if (!verifySignature(req.body, signature, process.env.WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature');
}
// ... process the verified webhook
});
Using timingSafeEqual instead of a plain string comparison prevents timing attacks that could otherwise leak information about the correct signature.
Step 2 — Never Trust Payload Content Without Verification
Without signature verification, anyone who discovers your webhook URL could send fake events — e.g. fake "payment succeeded" events to a payment webhook, potentially granting unauthorized access or products.
Step 3 — Respond Quickly, Process Asynchronously
app.post('/webhooks/provider', (req, res) => {
// Verify signature first
res.status(200).send('OK'); // respond immediately
processWebhookAsync(req.body); // handle the actual work separately
});
Most webhook providers expect a fast response (often within a few seconds) and will retry if they don't receive one — offload actual processing to a background job/queue rather than blocking the response.
Step 4 — Implement Idempotency
Webhook providers often retry delivery, meaning your endpoint might receive the same event more than once. See How to Implement Idempotent API Endpoints — track processed event IDs and skip duplicates.
const processedEvents = new Set(); // use a persistent store in production, not in-memory
if (processedEvents.has(event.id)) {
return res.status(200).send('Already processed');
}
processedEvents.add(event.id);
Step 5 — Use a Non-Guessable Endpoint Path (Defense in Depth)
While signature verification is the real security control, using an unpredictable path adds a minor additional layer:
/webhooks/a8f3e9c2-4b1d-4f7a-9e3c-8d2b1a6f5e4c/provider
Step 6 — Log All Incoming Webhook Events
logger.info('Webhook received', { provider: 'stripe', eventId: event.id, type: event.type });
Essential for debugging when something goes wrong, and for auditing what events your system actually received and processed.
Step 7 — Set Up Monitoring for Webhook Failures
Alert if webhook processing consistently fails — a broken webhook integration can silently cause missed events (payments, order updates) for an extended period without any obvious symptom.
Step 8 — Rate Limit Webhook Endpoints Too
Even trusted webhook sources should be rate-limited as a safety measure against misconfiguration on their end or an actual attack attempting to exploit the endpoint.
Testing Webhooks Locally During Development
Use a tunneling tool (search for options like ngrok or similar) to expose your local development server temporarily, letting the external service's webhooks reach your machine during development without deploying first.
Common Errors
Signature verification always fails — ensure you're computing the signature over the raw request body, not a re-serialized/parsed version, since even whitespace differences change the signature.
Duplicate processing despite idempotency logic — verify the deduplication store persists across application restarts (use a database or Redis, not an in-memory Set, for production).
Best Practices
- Always verify signatures using a timing-safe comparison
- Respond quickly and process asynchronously
- Implement idempotency to handle provider retries safely
Continue Reading
- How to Implement Idempotent API Endpoints
- How to Build and Secure a REST API on a VPS
- How to Deploy Automatically on Git Push (Webhook-Based Deployment)
Browse more articles in Object Storage, Messaging & APIs.
