Designing Webhook Consumers That Survive Piisend Retries
Learn how to build robust webhook handlers for Piisend delivery events, focusing on idempotency, signature verification, and proper HTTP response codes to manage retries and timeouts effectively.
Section 1
Understanding Piisend Webhook Delivery and Retries
Piisend webhooks provide real-time notifications about your email delivery events, such as bounces, opens, and clicks. However, network issues or temporary outages on your server can prevent immediate delivery. To ensure event-driven email reliability, Piisend implements a robust retry mechanism, attempting to redeliver events if your endpoint doesn't respond successfully within a timeout period. This means your webhook consumer must be designed to handle multiple delivery attempts for the same event.
1{2 "event_id": "evt_1234567890abcdef",3 "event_type": "email.delivered",4 "timestamp": 1678886400,5 "payload": {6 "email_id": "eml_abcdef1234567890",7 "to": "recipient@example.com",8 "subject": "Your order has shipped!",9 "status": "delivered",10 "delivery_time": 167888639511 }12}Section 2
Building Idempotent Webhook Handlers for Reliability
Due to Piisend's retry policy, your webhook endpoint might receive the same event multiple times. To prevent duplicate processing (e.g., sending multiple notifications for a single email delivery), your handler must be idempotent. This means that processing the same event multiple times should have the same effect as processing it once. You can achieve this by storing and checking a unique identifier, like the event_id provided in the webhook payload, before performing any actions.
1import json2import os3 4def handle_webhook_event(event_data):5 event_id = event_data.get("event_id")6 if not event_id:7 print("Error: Missing event_id in webhook payload")8 return 4009 10 # In a real application, use a database or cache to store processed event IDs11 # For demonstration, we'll use a simple in-memory set (not suitable for production)12 processed_events = set()13 14 if event_id in processed_events:15 print(f"Event {event_id} already processed. Skipping.")16 return 200 # Acknowledge success even if already processed17 18 # Simulate processing the event19 print(f"Processing event: {event_id} - Type: {event_data.get('event_type')}")20 processed_events.add(event_id)21 22 # Perform actual business logic here, e.g., update user status, log delivery23 # ...24 25 return 20026 27# Example usage (in a Flask/Django view or similar)28# event_payload = json.loads(request.data)29# status_code = handle_webhook_event(event_payload)30# return Response(status=status_code)Section 3
Verifying Piisend Webhook Signatures and Timestamps
Security is paramount for webhooks. You must verify that incoming requests genuinely originate from Piisend and haven't been tampered with. Piisend sends two crucial headers: X-Webhook-Signature (an HMAC-SHA256 hash of the raw request body, signed with your webhook secret) and X-Webhook-Timestamp. Verifying the signature ensures authenticity, while checking the timestamp helps protect against replay attacks by rejecting requests that are too old.
1const crypto = require('crypto');2 3const WEBHOOK_SECRET = process.env.PIISEND_WEBHOOK_SECRET;4 5function verifyWebhookSignature(requestBody, signature, timestamp) {6 if (!WEBHOOK_SECRET) {7 throw new Error('PIISEND_WEBHOOK_SECRET is not set.');8 }9 10 // Check for replay attacks (e.g., within 5 minutes)11 const FIVE_MINUTES_IN_SECONDS = 5 * 60;12 const now = Math.floor(Date.now() / 1000);13 if (Math.abs(now - timestamp) > FIVE_MINUTES_IN_SECONDS) {14 console.warn('Webhook timestamp is too old or in the future.');15 return false;16 }17 18 const signedPayload = `${timestamp}.${requestBody}`;19 const expectedSignature = crypto20 .createHmac('sha256', WEBHOOK_SECRET)21 .update(signedPayload)22 .digest('hex');23 24 return crypto.timingSafeEqual(25 Buffer.from(signature, 'utf8'),26 Buffer.from(expectedSignature, 'utf8')27 );28}29 30// Example usage in an Express.js route:31// app.post('/webhook', (req, res) => {32// const signature = req.headers['x-webhook-signature'];33// const timestamp = parseInt(req.headers['x-webhook-timestamp'], 10);34// const rawBody = req.rawBody; // Ensure you have raw body access, e.g., with body-parser.raw()35 36// if (!verifyWebhookSignature(rawBody, signature, timestamp)) {37// return res.status(401).send('Invalid webhook signature or timestamp');38// }39 40// // Process the webhook event41// res.status(200).send('OK');42// });Section 4
How Your HTTP Responses Affect Piisend Retries
The HTTP status code your webhook endpoint returns dictates how Piisend handles retries. A 2xx status (e.g., 200 OK) signals successful receipt and processing, stopping any further retries for that event. A 4xx status (e.g., 400 Bad Request, 401 Unauthorized) indicates a permanent client-side error, and Piisend will typically cease retrying. Conversely, a 5xx status (e.g., 500 Internal Server Error, 503 Service Unavailable) tells Piisend that a temporary server-side issue occurred, prompting it to retry the webhook delivery after a delay.
1HTTP/1.1 200 OK2Content-Type: text/plain3Content-Length: 24 5OK