Building Reliable Webhook Handlers for Email Delivery Events

The Reality of Webhooks in Production

When sending mission-critical messages through a transactional email API, real-time event feedback is essential:

  • Has the user's password reset email landed in their inbox?
  • Did an invoice email bounce because of a typo in the customer's email address?
  • Did the client click the verification link?

Email providers communicate these events via HTTP Webhooks. However, in a distributed production environment:

1. Webhooks can arrive out of order (e.g., opened arriving milliseconds before delivered).

2. Webhooks can be delivered more than once due to network timeouts.

3. Webhook endpoints are publicly accessible, requiring HMAC cryptographic verification.


1. Verifying HMAC-SHA256 Signatures

To ensure an incoming HTTP POST request actually originated from SendPorter (and not an attacker forging a delivery event), every webhook payload includes an HMAC signature header:

http
POST /api/webhooks/sendporter HTTP/1.1
X-SendPorter-Signature: t=1728000000,v1=9f83a...
Content-Type: application/json

Verification in Node.js / Express

javascript
import crypto from "crypto";

function verifyWebhook(rawBody, signatureHeader, secret) {
  const parts = Object.fromEntries(
    signatureHeader.split(",").map(item => item.split("="))
  );
  const timestamp = parts.t;
  const signature = parts.v1;

  // Protect against replay attacks (ignore payloads > 5 minutes old)
  if (Math.abs(Date.now() / 1000 - parseInt(timestamp, 10)) > 300) {
    return false;
  }

  const expectedSignature = crypto
    .createHmac("sha256", secret)
    .update(`${timestamp}.${rawBody}`)
    .digest("hex");

  return crypto.timingSafeEqual(
    Buffer.from(signature, "hex"),
    Buffer.from(expectedSignature, "hex")
  );
}

2. Idempotency: Handling Duplicate Deliveries

Because webhooks use "at-least-once" delivery semantics, an unstable network connection might cause SendPorter to retry an event that your server already processed.

Never execute business logic directly on receipt of a webhook without checking an idempotency key.

Recommended Schema Pattern (SQL)

sql
CREATE TABLE processed_webhook_events (
  event_id UUID PRIMARY KEY,
  message_id UUID NOT NULL,
  event_type VARCHAR(32) NOT NULL,
  processed_at TIMESTAMPTZ DEFAULT NOW()
);

Before updating user status, run:

sql
INSERT INTO processed_webhook_events (event_id, message_id, event_type)
VALUES ($1, $2, $3)
ON CONFLICT (event_id) DO NOTHING;

If zero rows were inserted, immediately return HTTP 200 OK and terminate execution.


3. Handling Out-of-Order Delivery Receipts

In high-throughput pipelines, an opened event might reach your server before the delivered event completes its network hop.

Do not assume a linear state machine. Instead, store events as an append-only timeline keyed by message_id, or update states conditionally:

sql
-- Update status only if the new status is of higher precedence
UPDATE user_invitations
SET delivery_status = 'opened', updated_at = NOW()
WHERE email_message_id = $1
  AND delivery_status NOT IN ('clicked');

4. The 200-OK Contract

Your webhook consumer must respond with a 2xx status code within 3,000ms. If your handler takes too long (e.g. running complex reports or updating external CRMs), the webhook sender will treat the request as timed out and schedule a retry.

Best Practice:

1. Verify the signature.

2. Push the raw payload into a background queue (e.g., Redis, SQS, RabbitMQ, BullMQ).

3. Return HTTP 202 Accepted or HTTP 200 OK immediately.

4. Process the heavy database updates in your worker tier.

Send email with parcel-like visibility.

Join developer teams using SendPorter's REST API and SMTP relay for 100% DKIM-signed delivery.

Request access