Skip to content

Email Delivery Monitoring Dashboard

This use case demonstrates a fan-in pattern where delivery, bounce, and complaint webhooks from three email providers—Resend, SendGrid, and Postmark—are normalized into a common schema and routed to a monitoring API and Slack alerts channel.

Architecture

mermaid
flowchart LR
    Resend[Resend Webhooks] --> S1[Source: Resend]
    SendGrid[SendGrid Webhooks] --> S2[Source: SendGrid]
    Postmark[Postmark Webhooks] --> S3[Source: Postmark]
    S1 --> R1[Route: Monitoring]
    S1 --> R2[Route: Slack Alerts]
    S2 --> R3[Route: Monitoring]
    S2 --> R4[Route: Slack Alerts]
    S3 --> R5[Route: Monitoring]
    S3 --> R6[Route: Slack Alerts]
    R1 --> |Transform| D1[Monitoring API]
    R2 --> |Transform + Filter| D2[Slack #email-alerts]
    R3 --> |Transform| D1
    R4 --> |Transform + Filter| D2
    R5 --> |Transform| D1
    R6 --> |Transform + Filter| D2

Flow:

  1. Three email providers send delivery/bounce/complaint webhooks to separate Hookbase sources
  2. Each source verifies the provider's signature
  3. Routes normalize each provider's schema into a common format
  4. All delivery events flow to a single monitoring API
  5. Bounces and complaints trigger Slack alerts

Key Challenge

Each provider uses a different payload format. SendGrid batches multiple events in a single array payload. The transforms handle all three schemas and normalize them into one unified structure.

Step 1: Create Sources

Create a source for each email provider with signature verification.

Resend Source

bash
curl -X POST https://api.hookbase.app/api/organizations/{orgId}/sources \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Resend Production",
    "slug": "resend-prod",
    "description": "Resend email delivery webhooks",
    "verificationConfig": {
      "type": "svix",
      "secret": "whsec_your_resend_webhook_signing_secret"
    }
  }'

Response:

json
{
  "data": {
    "id": "src_resend01",
    "name": "Resend Production",
    "slug": "resend-prod",
    "url": "https://api.hookbase.app/ingest/your-org/resend-prod",
    "verificationConfig": {
      "type": "svix"
    },
    "createdAt": "2026-03-07T10:00:00Z"
  }
}

TIP

Resend uses Svix under the hood for webhook delivery. Use the svix verification type with the signing secret from your Resend dashboard under Webhooks.

SendGrid Source

bash
curl -X POST https://api.hookbase.app/api/organizations/{orgId}/sources \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "SendGrid Production",
    "slug": "sendgrid-prod",
    "description": "SendGrid Event Webhook",
    "verificationConfig": {
      "type": "sendgrid",
      "publicKey": "YOUR_SENDGRID_VERIFICATION_KEY"
    }
  }'

Response:

json
{
  "data": {
    "id": "src_sgrid01",
    "name": "SendGrid Production",
    "slug": "sendgrid-prod",
    "url": "https://api.hookbase.app/ingest/your-org/sendgrid-prod",
    "verificationConfig": {
      "type": "sendgrid"
    },
    "createdAt": "2026-03-07T10:01:00Z"
  }
}

WARNING

SendGrid sends events as a JSON array containing multiple events in a single request. The transform in Step 4 handles this by iterating over the array.

Postmark Source

bash
curl -X POST https://api.hookbase.app/api/organizations/{orgId}/sources \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Postmark Production",
    "slug": "postmark-prod",
    "description": "Postmark delivery and bounce webhooks",
    "verificationConfig": {
      "type": "hmac",
      "header": "X-Postmark-Webhook-Token",
      "secret": "your_postmark_webhook_token",
      "algorithm": "plain"
    }
  }'

Response:

json
{
  "data": {
    "id": "src_pmrk01",
    "name": "Postmark Production",
    "slug": "postmark-prod",
    "url": "https://api.hookbase.app/ingest/your-org/postmark-prod",
    "verificationConfig": {
      "type": "hmac"
    },
    "createdAt": "2026-03-07T10:02:00Z"
  }
}

TIP

Configure this URL in your Postmark server's Webhooks settings. Select the Delivery, Bounce, Spam Complaint, and Open triggers.

Step 2: Create Destinations

Monitoring API Destination

This is the central endpoint that receives all normalized email events:

bash
curl -X POST https://api.hookbase.app/api/organizations/{orgId}/destinations \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Email Monitoring API",
    "type": "http",
    "config": {
      "url": "https://monitoring.yourcompany.com/api/email-events",
      "method": "POST",
      "headers": {
        "Authorization": "Bearer YOUR_MONITORING_API_KEY",
        "Content-Type": "application/json"
      }
    },
    "retryConfig": {
      "maxAttempts": 5,
      "backoffMultiplier": 2,
      "initialInterval": 1000
    }
  }'

Response:

json
{
  "data": {
    "id": "dst_monitor01",
    "name": "Email Monitoring API",
    "type": "http",
    "createdAt": "2026-03-07T10:05:00Z"
  }
}

Slack Alerts Destination

Alert channel for bounces, complaints, and delivery failures:

bash
curl -X POST https://api.hookbase.app/api/organizations/{orgId}/destinations \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Slack #email-alerts",
    "type": "slack",
    "config": {
      "url": "https://hooks.slack.com/services/YOUR/WEBHOOK/URL"
    },
    "retryConfig": {
      "maxAttempts": 2,
      "backoffMultiplier": 1.5,
      "initialInterval": 500
    }
  }'

Response:

json
{
  "data": {
    "id": "dst_slack01",
    "name": "Slack #email-alerts",
    "type": "slack",
    "createdAt": "2026-03-07T10:06:00Z"
  }
}

Step 3: Create Routes with Filters

Each source needs two routes: one to the monitoring API (all events) and one to Slack (bounces and complaints only). That means six routes total.

Monitoring Routes (All Events)

These routes send all email events from each provider to the monitoring API. No filter is needed—every event is forwarded.

Resend to Monitoring:

bash
curl -X POST https://api.hookbase.app/api/organizations/{orgId}/routes \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Resend → Monitoring",
    "sourceId": "src_resend01",
    "destinationId": "dst_monitor01",
    "transformId": "tfm_resend_norm",
    "enabled": true
  }'

SendGrid to Monitoring:

bash
curl -X POST https://api.hookbase.app/api/organizations/{orgId}/routes \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "SendGrid → Monitoring",
    "sourceId": "src_sgrid01",
    "destinationId": "dst_monitor01",
    "transformId": "tfm_sgrid_norm",
    "enabled": true
  }'

Postmark to Monitoring:

bash
curl -X POST https://api.hookbase.app/api/organizations/{orgId}/routes \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Postmark → Monitoring",
    "sourceId": "src_pmrk01",
    "destinationId": "dst_monitor01",
    "transformId": "tfm_pmrk_norm",
    "enabled": true
  }'

Slack Alert Routes (Bounces and Complaints Only)

Create a shared filter for bounce and complaint events, then use it for all three Slack routes.

Bounce and Complaint Filter for Resend:

bash
curl -X POST https://api.hookbase.app/api/organizations/{orgId}/filters \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Resend Bounces & Complaints",
    "conditions": [
      {
        "field": "type",
        "operator": "in",
        "value": ["email.bounced", "email.complained", "email.delivery_delayed"]
      }
    ],
    "logic": "AND"
  }'

Response:

json
{
  "data": {
    "id": "flt_resend_alert",
    "name": "Resend Bounces & Complaints"
  }
}

Bounce and Complaint Filter for SendGrid:

bash
curl -X POST https://api.hookbase.app/api/organizations/{orgId}/filters \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "SendGrid Bounces & Complaints",
    "conditions": [
      {
        "field": "[0].event",
        "operator": "in",
        "value": ["bounce", "dropped", "spamreport", "deferred"]
      }
    ],
    "logic": "AND"
  }'

Response:

json
{
  "data": {
    "id": "flt_sgrid_alert",
    "name": "SendGrid Bounces & Complaints"
  }
}

TIP

SendGrid payloads are arrays, so the filter targets [0].event to check the first event in the batch. The transform will process the full array.

Bounce and Complaint Filter for Postmark:

bash
curl -X POST https://api.hookbase.app/api/organizations/{orgId}/filters \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Postmark Bounces & Complaints",
    "conditions": [
      {
        "field": "RecordType",
        "operator": "in",
        "value": ["Bounce", "SpamComplaint"]
      }
    ],
    "logic": "AND"
  }'

Response:

json
{
  "data": {
    "id": "flt_pmrk_alert",
    "name": "Postmark Bounces & Complaints"
  }
}

Create the three Slack routes:

bash
# Resend → Slack
curl -X POST https://api.hookbase.app/api/organizations/{orgId}/routes \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Resend → Slack Alerts",
    "sourceId": "src_resend01",
    "destinationId": "dst_slack01",
    "filterId": "flt_resend_alert",
    "transformId": "tfm_resend_slack",
    "enabled": true
  }'

# SendGrid → Slack
curl -X POST https://api.hookbase.app/api/organizations/{orgId}/routes \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "SendGrid → Slack Alerts",
    "sourceId": "src_sgrid01",
    "destinationId": "dst_slack01",
    "filterId": "flt_sgrid_alert",
    "transformId": "tfm_sgrid_slack",
    "enabled": true
  }'

# Postmark → Slack
curl -X POST https://api.hookbase.app/api/organizations/{orgId}/routes \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Postmark → Slack Alerts",
    "sourceId": "src_pmrk01",
    "destinationId": "dst_slack01",
    "filterId": "flt_pmrk_alert",
    "transformId": "tfm_pmrk_slack",
    "enabled": true
  }'

Step 4: Add Transforms

This is where the fan-in normalization happens. Each provider has a different payload format, and we need to map them all into a common schema.

Common normalized schema:

json
{
  "provider": "resend | sendgrid | postmark",
  "event_type": "delivered | bounced | complained | opened | clicked | deferred",
  "message_id": "provider-specific-id",
  "recipient": "[email protected]",
  "subject": "Email subject line",
  "timestamp": "2026-03-07T10:30:00Z",
  "metadata": {
    "bounce_type": "hard | soft",
    "bounce_reason": "description",
    "smtp_code": 550
  }
}

Resend Monitoring Transform

Resend sends individual events with a flat structure:

bash
curl -X POST https://api.hookbase.app/api/organizations/{orgId}/transforms \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Resend → Normalized Email Event",
    "type": "jsonata",
    "config": {
      "expression": "(\n  $eventMap := {\n    \"email.sent\": \"sent\",\n    \"email.delivered\": \"delivered\",\n    \"email.bounced\": \"bounced\",\n    \"email.complained\": \"complained\",\n    \"email.opened\": \"opened\",\n    \"email.clicked\": \"clicked\",\n    \"email.delivery_delayed\": \"deferred\"\n  };\n  {\n    \"provider\": \"resend\",\n    \"event_type\": $lookup($eventMap, type),\n    \"message_id\": data.email_id,\n    \"recipient\": data.to[0],\n    \"subject\": data.subject,\n    \"timestamp\": data.created_at,\n    \"metadata\": type = \"email.bounced\" ? {\n      \"bounce_type\": data.bounce.type,\n      \"bounce_reason\": data.bounce.message,\n      \"smtp_code\": data.bounce.status_code\n    } : type = \"email.complained\" ? {\n      \"complaint_type\": data.complaint.complaint_type,\n      \"feedback_id\": data.complaint.feedback_id\n    } : type = \"email.clicked\" ? {\n      \"click_url\": data.click.link,\n      \"user_agent\": data.click.user_agent\n    } : {}\n  }\n)"
    }
  }'

Example Input (Resend email.bounced):

json
{
  "type": "email.bounced",
  "created_at": "2026-03-07T10:30:00Z",
  "data": {
    "email_id": "re_abc123xyz",
    "to": ["[email protected]"],
    "subject": "Your invoice is ready",
    "bounce": {
      "type": "hard",
      "message": "Mailbox does not exist",
      "status_code": 550
    }
  }
}

Example Output:

json
{
  "provider": "resend",
  "event_type": "bounced",
  "message_id": "re_abc123xyz",
  "recipient": "[email protected]",
  "subject": "Your invoice is ready",
  "timestamp": "2026-03-07T10:30:00Z",
  "metadata": {
    "bounce_type": "hard",
    "bounce_reason": "Mailbox does not exist",
    "smtp_code": 550
  }
}

SendGrid Monitoring Transform

SendGrid sends events as a JSON array with multiple events per request. The transform iterates over the array and outputs one normalized event per item:

bash
curl -X POST https://api.hookbase.app/api/organizations/{orgId}/transforms \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "SendGrid → Normalized Email Events",
    "type": "jsonata",
    "config": {
      "expression": "(\n  $eventMap := {\n    \"processed\": \"sent\",\n    \"delivered\": \"delivered\",\n    \"bounce\": \"bounced\",\n    \"dropped\": \"bounced\",\n    \"deferred\": \"deferred\",\n    \"open\": \"opened\",\n    \"click\": \"clicked\",\n    \"spamreport\": \"complained\"\n  };\n  $map($, function($evt) {\n    {\n      \"provider\": \"sendgrid\",\n      \"event_type\": $lookup($eventMap, $evt.event),\n      \"message_id\": $evt.sg_message_id,\n      \"recipient\": $evt.email,\n      \"subject\": $evt.subject,\n      \"timestamp\": $fromMillis($evt.timestamp * 1000),\n      \"metadata\": $evt.event = \"bounce\" or $evt.event = \"dropped\" ? {\n        \"bounce_type\": $evt.type,\n        \"bounce_reason\": $evt.reason,\n        \"smtp_code\": $evt.status\n      } : $evt.event = \"click\" ? {\n        \"click_url\": $evt.url,\n        \"user_agent\": $evt.useragent\n      } : $evt.event = \"spamreport\" ? {\n        \"complaint_type\": \"spam\",\n        \"feedback_id\": $evt.sg_event_id\n      } : {}\n    }\n  })\n)"
    }
  }'

Important: SendGrid Batch Payloads

SendGrid sends up to 1,000 events per request as a JSON array. This transform uses $map() to iterate over every event in the batch and produces an array of normalized events. Your monitoring API must handle both single objects (from Resend/Postmark) and arrays (from SendGrid).

Example Input (SendGrid batch with 2 events):

json
[
  {
    "email": "[email protected]",
    "event": "delivered",
    "sg_message_id": "sg_msg_001.filter0001",
    "subject": "Welcome aboard",
    "timestamp": 1741340400,
    "sg_event_id": "sg_evt_001"
  },
  {
    "email": "[email protected]",
    "event": "bounce",
    "sg_message_id": "sg_msg_002.filter0001",
    "subject": "Your receipt",
    "timestamp": 1741340410,
    "type": "hard",
    "reason": "550 5.1.1 The email account does not exist",
    "status": "550",
    "sg_event_id": "sg_evt_002"
  }
]

Example Output:

json
[
  {
    "provider": "sendgrid",
    "event_type": "delivered",
    "message_id": "sg_msg_001.filter0001",
    "recipient": "[email protected]",
    "subject": "Welcome aboard",
    "timestamp": "2026-03-07T10:20:00Z",
    "metadata": {}
  },
  {
    "provider": "sendgrid",
    "event_type": "bounced",
    "message_id": "sg_msg_002.filter0001",
    "recipient": "[email protected]",
    "subject": "Your receipt",
    "timestamp": "2026-03-07T10:20:10Z",
    "metadata": {
      "bounce_type": "hard",
      "bounce_reason": "550 5.1.1 The email account does not exist",
      "smtp_code": "550"
    }
  }
]

Postmark Monitoring Transform

Postmark sends individual events with PascalCase field names:

bash
curl -X POST https://api.hookbase.app/api/organizations/{orgId}/transforms \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Postmark → Normalized Email Event",
    "type": "jsonata",
    "config": {
      "expression": "(\n  $eventMap := {\n    \"Delivery\": \"delivered\",\n    \"Bounce\": \"bounced\",\n    \"SpamComplaint\": \"complained\",\n    \"Open\": \"opened\",\n    \"Click\": \"clicked\"\n  };\n  {\n    \"provider\": \"postmark\",\n    \"event_type\": $lookup($eventMap, RecordType),\n    \"message_id\": MessageID,\n    \"recipient\": Recipient ? Recipient : Email,\n    \"subject\": Subject,\n    \"timestamp\": DeliveredAt ? DeliveredAt : BouncedAt ? BouncedAt : ReceivedAt,\n    \"metadata\": RecordType = \"Bounce\" ? {\n      \"bounce_type\": Type = 1 ? \"hard\" : \"soft\",\n      \"bounce_reason\": Description,\n      \"smtp_code\": TypeCode\n    } : RecordType = \"SpamComplaint\" ? {\n      \"complaint_type\": \"spam\",\n      \"feedback_id\": ID\n    } : RecordType = \"Click\" ? {\n      \"click_url\": OriginalLink,\n      \"user_agent\": UserAgent\n    } : {}\n  }\n)"
    }
  }'

Example Input (Postmark Bounce):

json
{
  "RecordType": "Bounce",
  "Type": 1,
  "TypeCode": 1,
  "MessageID": "pm-msg-abc123",
  "Description": "Hard bounce - address does not exist",
  "Email": "[email protected]",
  "Subject": "Monthly report",
  "BouncedAt": "2026-03-07T10:35:00Z",
  "ServerID": 12345,
  "Tag": "monthly-report",
  "ID": 98765432
}

Example Output:

json
{
  "provider": "postmark",
  "event_type": "bounced",
  "message_id": "pm-msg-abc123",
  "recipient": "[email protected]",
  "subject": "Monthly report",
  "timestamp": "2026-03-07T10:35:00Z",
  "metadata": {
    "bounce_type": "hard",
    "bounce_reason": "Hard bounce - address does not exist",
    "smtp_code": 1
  }
}

Slack Alert Transforms

Each provider needs a Slack transform that formats bounce/complaint events as Block Kit messages.

Resend Slack Transform:

bash
curl -X POST https://api.hookbase.app/api/organizations/{orgId}/transforms \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Resend → Slack Alert",
    "type": "jsonata",
    "config": {
      "expression": "(\n  $isBounce := type = \"email.bounced\";\n  $isComplaint := type = \"email.complained\";\n  $emoji := $isBounce ? \":warning:\" : $isComplaint ? \":rotating_light:\" : \":hourglass:\";\n  $label := $isBounce ? \"Email Bounced\" : $isComplaint ? \"Spam Complaint\" : \"Delivery Delayed\";\n  $color := $isBounce ? \"#e74c3c\" : $isComplaint ? \"#e91e63\" : \"#f39c12\";\n  {\n    \"attachments\": [\n      {\n        \"color\": $color,\n        \"blocks\": [\n          {\n            \"type\": \"header\",\n            \"text\": {\n              \"type\": \"plain_text\",\n              \"text\": $emoji & \" \" & $label & \" (Resend)\"\n            }\n          },\n          {\n            \"type\": \"section\",\n            \"fields\": [\n              {\n                \"type\": \"mrkdwn\",\n                \"text\": \"*Recipient:*\\n\" & data.to[0]\n              },\n              {\n                \"type\": \"mrkdwn\",\n                \"text\": \"*Subject:*\\n\" & data.subject\n              },\n              {\n                \"type\": \"mrkdwn\",\n                \"text\": \"*Reason:*\\n\" & ($isBounce ? data.bounce.message : $isComplaint ? data.complaint.complaint_type : \"Temporary delivery issue\")\n              },\n              {\n                \"type\": \"mrkdwn\",\n                \"text\": \"*Time:*\\n\" & data.created_at\n              }\n            ]\n          },\n          {\n            \"type\": \"context\",\n            \"elements\": [\n              {\n                \"type\": \"mrkdwn\",\n                \"text\": \"Message ID: \" & data.email_id & \" | Provider: Resend\"\n              }\n            ]\n          }\n        ]\n      }\n    ]\n  }\n)"
    }
  }'

SendGrid Slack Transform:

This transform extracts only bounce/complaint events from the SendGrid batch array and formats them:

bash
curl -X POST https://api.hookbase.app/api/organizations/{orgId}/transforms \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "SendGrid → Slack Alert",
    "type": "jsonata",
    "config": {
      "expression": "(\n  $alerts := $filter($, function($evt) {\n    $evt.event in [\"bounce\", \"dropped\", \"spamreport\", \"deferred\"]\n  });\n  $count := $count($alerts);\n  $first := $alerts[0];\n  $isBounce := $first.event = \"bounce\" or $first.event = \"dropped\";\n  $isSpam := $first.event = \"spamreport\";\n  $emoji := $isBounce ? \":warning:\" : $isSpam ? \":rotating_light:\" : \":hourglass:\";\n  $label := $isBounce ? \"Email Bounced\" : $isSpam ? \"Spam Report\" : \"Delivery Deferred\";\n  $color := $isBounce ? \"#e74c3c\" : $isSpam ? \"#e91e63\" : \"#f39c12\";\n  {\n    \"attachments\": [\n      {\n        \"color\": $color,\n        \"blocks\": [\n          {\n            \"type\": \"header\",\n            \"text\": {\n              \"type\": \"plain_text\",\n              \"text\": $emoji & \" \" & $label & \" (SendGrid)\" & ($count > 1 ? \" +\" & $string($count - 1) & \" more\" : \"\")\n            }\n          },\n          {\n            \"type\": \"section\",\n            \"fields\": [\n              {\n                \"type\": \"mrkdwn\",\n                \"text\": \"*Recipient:*\\n\" & $first.email\n              },\n              {\n                \"type\": \"mrkdwn\",\n                \"text\": \"*Subject:*\\n\" & $first.subject\n              },\n              {\n                \"type\": \"mrkdwn\",\n                \"text\": \"*Reason:*\\n\" & ($first.reason ? $first.reason : $first.event)\n              },\n              {\n                \"type\": \"mrkdwn\",\n                \"text\": \"*Events in batch:*\\n\" & $string($count)\n              }\n            ]\n          },\n          {\n            \"type\": \"context\",\n            \"elements\": [\n              {\n                \"type\": \"mrkdwn\",\n                \"text\": \"Message ID: \" & $first.sg_message_id & \" | Provider: SendGrid\"\n              }\n            ]\n          }\n        ]\n      }\n    ]\n  }\n)"
    }
  }'

Postmark Slack Transform:

bash
curl -X POST https://api.hookbase.app/api/organizations/{orgId}/transforms \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Postmark → Slack Alert",
    "type": "jsonata",
    "config": {
      "expression": "(\n  $isBounce := RecordType = \"Bounce\";\n  $isComplaint := RecordType = \"SpamComplaint\";\n  $emoji := $isBounce ? \":warning:\" : \":rotating_light:\";\n  $label := $isBounce ? \"Email Bounced\" : \"Spam Complaint\";\n  $color := $isBounce ? \"#e74c3c\" : \"#e91e63\";\n  {\n    \"attachments\": [\n      {\n        \"color\": $color,\n        \"blocks\": [\n          {\n            \"type\": \"header\",\n            \"text\": {\n              \"type\": \"plain_text\",\n              \"text\": $emoji & \" \" & $label & \" (Postmark)\"\n            }\n          },\n          {\n            \"type\": \"section\",\n            \"fields\": [\n              {\n                \"type\": \"mrkdwn\",\n                \"text\": \"*Recipient:*\\n\" & Email\n              },\n              {\n                \"type\": \"mrkdwn\",\n                \"text\": \"*Subject:*\\n\" & Subject\n              },\n              {\n                \"type\": \"mrkdwn\",\n                \"text\": \"*Reason:*\\n\" & ($isBounce ? Description : \"User marked as spam\")\n              },\n              {\n                \"type\": \"mrkdwn\",\n                \"text\": \"*Bounce Type:*\\n\" & ($isBounce ? (Type = 1 ? \"Hard bounce\" : \"Soft bounce\") : \"N/A\")\n              }\n            ]\n          },\n          {\n            \"type\": \"context\",\n            \"elements\": [\n              {\n                \"type\": \"mrkdwn\",\n                \"text\": \"Message ID: \" & MessageID & \" | Provider: Postmark | Server: \" & $string(ServerID)\"\n              }\n            ]\n          }\n        ]\n      }\n    ]\n  }\n)"
    }
  }'

Example Slack Output (Postmark Bounce):

json
{
  "attachments": [
    {
      "color": "#e74c3c",
      "blocks": [
        {
          "type": "header",
          "text": {
            "type": "plain_text",
            "text": ":warning: Email Bounced (Postmark)"
          }
        },
        {
          "type": "section",
          "fields": [
            {
              "type": "mrkdwn",
              "text": "*Recipient:*\n[email protected]"
            },
            {
              "type": "mrkdwn",
              "text": "*Subject:*\nMonthly report"
            },
            {
              "type": "mrkdwn",
              "text": "*Reason:*\nHard bounce - address does not exist"
            },
            {
              "type": "mrkdwn",
              "text": "*Bounce Type:*\nHard bounce"
            }
          ]
        },
        {
          "type": "context",
          "elements": [
            {
              "type": "mrkdwn",
              "text": "Message ID: pm-msg-abc123 | Provider: Postmark | Server: 12345"
            }
          ]
        }
      ]
    }
  ]
}

Step 5: Test the Pipeline

Send test payloads to each source to verify the full pipeline.

Test Resend Webhook

bash
curl -X POST https://api.hookbase.app/ingest/your-org/resend-prod \
  -H "Content-Type: application/json" \
  -H "svix-id: test_msg_001" \
  -H "svix-timestamp: 1741340400" \
  -H "svix-signature: v1,test_signature" \
  -d '{
    "type": "email.bounced",
    "created_at": "2026-03-07T10:30:00Z",
    "data": {
      "email_id": "re_test_bounce_001",
      "from": "[email protected]",
      "to": ["[email protected]"],
      "subject": "Your invoice #1234",
      "bounce": {
        "type": "hard",
        "message": "550 5.1.1 Mailbox does not exist",
        "status_code": 550
      },
      "created_at": "2026-03-07T10:30:00Z"
    }
  }'

Test SendGrid Webhook (Batch Payload)

bash
curl -X POST https://api.hookbase.app/ingest/your-org/sendgrid-prod \
  -H "Content-Type: application/json" \
  -H "X-Twilio-Email-Event-Webhook-Signature: test_signature" \
  -H "X-Twilio-Email-Event-Webhook-Timestamp: 1741340400" \
  -d '[
    {
      "email": "[email protected]",
      "event": "delivered",
      "sg_message_id": "sg_test_001.filter0001",
      "subject": "Welcome to our platform",
      "timestamp": 1741340400,
      "sg_event_id": "sg_evt_test_001",
      "smtp-id": "<[email protected]>"
    },
    {
      "email": "[email protected]",
      "event": "bounce",
      "sg_message_id": "sg_test_002.filter0001",
      "subject": "Your account summary",
      "timestamp": 1741340410,
      "type": "hard",
      "reason": "550 5.1.1 The email account that you tried to reach does not exist",
      "status": "550",
      "sg_event_id": "sg_evt_test_002"
    },
    {
      "email": "[email protected]",
      "event": "delivered",
      "sg_message_id": "sg_test_003.filter0001",
      "subject": "Password reset",
      "timestamp": 1741340420,
      "sg_event_id": "sg_evt_test_003"
    }
  ]'

Test Postmark Webhook

bash
curl -X POST https://api.hookbase.app/ingest/your-org/postmark-prod \
  -H "Content-Type: application/json" \
  -H "X-Postmark-Webhook-Token: your_postmark_webhook_token" \
  -d '{
    "RecordType": "Bounce",
    "Type": 1,
    "TypeCode": 1,
    "MessageID": "pm_test_bounce_001",
    "Description": "Hard bounce - The email account does not exist",
    "Email": "[email protected]",
    "Subject": "Order confirmation #5678",
    "BouncedAt": "2026-03-07T10:35:00Z",
    "ServerID": 12345,
    "Tag": "order-confirmation",
    "ID": 99990001
  }'

Verify Deliveries

Check that events were delivered to both destinations:

bash
curl https://api.hookbase.app/api/organizations/{orgId}/deliveries?limit=20 \
  -H "Authorization: Bearer YOUR_TOKEN"

You should see deliveries for each test:

  • Resend test: 2 deliveries (Monitoring API + Slack alert for bounce)
  • SendGrid test: 2 deliveries (Monitoring API with 3 normalized events + Slack alert for bounce)
  • Postmark test: 2 deliveries (Monitoring API + Slack alert for bounce)

TIP

Use the Events page in the Hookbase dashboard to inspect the raw and transformed payloads side by side for each delivery.

Production Considerations

1. Enable Deduplication on All Sources

Email providers may retry webhook delivery. Prevent duplicate processing:

bash
# Resend - deduplicate by Svix message ID (available in headers)
curl -X PATCH https://api.hookbase.app/api/organizations/{orgId}/sources/src_resend01 \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "deduplicationConfig": {
      "enabled": true,
      "keyPath": "data.email_id",
      "windowSeconds": 86400
    }
  }'

# SendGrid - deduplicate by sg_event_id from first event in batch
curl -X PATCH https://api.hookbase.app/api/organizations/{orgId}/sources/src_sgrid01 \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "deduplicationConfig": {
      "enabled": true,
      "keyPath": "[0].sg_event_id",
      "windowSeconds": 86400
    }
  }'

# Postmark - deduplicate by MessageID
curl -X PATCH https://api.hookbase.app/api/organizations/{orgId}/sources/src_pmrk01 \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "deduplicationConfig": {
      "enabled": true,
      "keyPath": "MessageID",
      "windowSeconds": 86400
    }
  }'

2. Configure Circuit Breakers

Protect your monitoring API from overload during email incidents (large bounce storms can generate thousands of events):

bash
curl -X PATCH https://api.hookbase.app/api/organizations/{orgId}/destinations/dst_monitor01 \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "circuitBreakerConfig": {
      "enabled": true,
      "failureThreshold": 15,
      "windowSeconds": 60,
      "resetTimeoutSeconds": 300
    }
  }'

3. Set Up Notification Channels

Get alerted when deliveries to your monitoring API fail:

bash
curl -X POST https://api.hookbase.app/api/organizations/{orgId}/notification-channels \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Email Pipeline Alerts",
    "type": "slack",
    "config": {
      "url": "https://hooks.slack.com/services/YOUR/OPS-ALERT/URL"
    },
    "events": ["delivery.failed", "destination.circuit_breaker.opened", "source.verification.failed"],
    "filters": {
      "sourceIds": ["src_resend01", "src_sgrid01", "src_pmrk01"],
      "destinationIds": ["dst_monitor01"]
    }
  }'

4. Set Up Failover for Monitoring

Ensure email events reach a backup if the primary monitoring API goes down:

bash
curl -X POST https://api.hookbase.app/api/organizations/{orgId}/destinations \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Monitoring API Backup",
    "type": "http",
    "config": {
      "url": "https://monitoring-backup.yourcompany.com/api/email-events",
      "method": "POST",
      "headers": {
        "Authorization": "Bearer YOUR_BACKUP_API_KEY",
        "Content-Type": "application/json"
      }
    }
  }'
bash
# Apply failover to all three monitoring routes
for route_id in rte_resend_mon rte_sgrid_mon rte_pmrk_mon; do
  curl -X PATCH https://api.hookbase.app/api/organizations/{orgId}/routes/${route_id} \
    -H "Authorization: Bearer YOUR_TOKEN" \
    -H "Content-Type: application/json" \
    -d '{
      "failoverConfig": {
        "enabled": true,
        "destinationId": "dst_backup01",
        "triggerAfterAttempts": 3
      }
    }'
done

5. Use Scoped API Keys

Create restricted API keys for each email provider's webhook configuration:

bash
curl -X POST https://api.hookbase.app/api/organizations/{orgId}/api-keys \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Email Provider Ingestion",
    "scopes": ["events:create"],
    "sourceIds": ["src_resend01", "src_sgrid01", "src_pmrk01"],
    "expiresAt": "2027-03-07T00:00:00Z"
  }'

See Also

Released under the MIT License.