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
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| D2Flow:
- Three email providers send delivery/bounce/complaint webhooks to separate Hookbase sources
- Each source verifies the provider's signature
- Routes normalize each provider's schema into a common format
- All delivery events flow to a single monitoring API
- 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
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:
{
"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
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:
{
"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
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:
{
"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:
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:
{
"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:
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:
{
"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:
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:
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:
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:
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:
{
"data": {
"id": "flt_resend_alert",
"name": "Resend Bounces & Complaints"
}
}Bounce and Complaint Filter for SendGrid:
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:
{
"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:
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:
{
"data": {
"id": "flt_pmrk_alert",
"name": "Postmark Bounces & Complaints"
}
}Create the three Slack routes:
# 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:
{
"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:
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):
{
"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:
{
"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:
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):
[
{
"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:
[
{
"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:
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):
{
"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:
{
"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:
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:
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:
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):
{
"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
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)
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
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:
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:
# 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):
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:
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:
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"
}
}
}'# 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
}
}'
done5. Use Scoped API Keys
Create restricted API keys for each email provider's webhook configuration:
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"
}'Related Guides
- Resend Integration - Resend-specific setup and Svix signature verification
- SendGrid Integration - SendGrid Event Webhook configuration
- Postmark Integration - Postmark webhook setup and token verification
- Transforms - JSONata syntax and advanced examples
- Filters - Complex routing logic and conditions
- Deduplication - Prevent duplicate event processing
- Failover - Ensure critical events are delivered
- Circuit Breaker - Protect downstream services
- Notification Channels - Configure delivery alerts
See Also
- SendGrid Integration — SendGrid webhook setup
- Postmark Integration — Postmark webhook setup
- Resend Integration — Resend webhook setup
- Transforms — Normalize email event data