$npx -y skills add hookdeck/webhook-skills --skill postmark-webhooksReceive and process Postmark webhooks. Use when setting up Postmark webhook handlers, handling email delivery events, processing bounces, opens, clicks, spam complaints, or subscription changes.
| 1 | # Postmark Webhooks |
| 2 | |
| 3 | ## When to Use This Skill |
| 4 | |
| 5 | - Setting up Postmark webhook handlers for email event tracking |
| 6 | - Processing email delivery events (bounce, delivered, open, click) |
| 7 | - Handling spam complaints and subscription changes |
| 8 | - Implementing email engagement analytics |
| 9 | - Troubleshooting webhook authentication issues |
| 10 | |
| 11 | ## Essential Code |
| 12 | |
| 13 | ### Authentication |
| 14 | |
| 15 | Postmark does NOT use signature verification. Instead, webhooks are authenticated by including credentials in the webhook URL itself. |
| 16 | |
| 17 | ```javascript |
| 18 | // Express - Basic Auth in URL |
| 19 | // Configure webhook URL in Postmark as: |
| 20 | // https://username:password@yourdomain.com/webhooks/postmark |
| 21 | |
| 22 | app.post('/webhooks/postmark', express.json(), (req, res) => { |
| 23 | // Basic auth is handled by your web server or proxy |
| 24 | // Additional validation can check expected payload structure |
| 25 | |
| 26 | const event = req.body; |
| 27 | |
| 28 | // Validate expected fields exist |
| 29 | if (!event.RecordType || !event.MessageID) { |
| 30 | return res.status(400).send('Invalid payload structure'); |
| 31 | } |
| 32 | |
| 33 | // Process event |
| 34 | console.log(`Received ${event.RecordType} event for ${event.Email}`); |
| 35 | |
| 36 | res.sendStatus(200); |
| 37 | }); |
| 38 | |
| 39 | // Alternative: Token in URL |
| 40 | // Configure webhook URL as: |
| 41 | // https://yourdomain.com/webhooks/postmark?token=your-secret-token |
| 42 | |
| 43 | app.post('/webhooks/postmark', express.json(), (req, res) => { |
| 44 | const token = req.query.token; |
| 45 | |
| 46 | if (token !== process.env.POSTMARK_WEBHOOK_TOKEN) { |
| 47 | return res.status(401).send('Unauthorized'); |
| 48 | } |
| 49 | |
| 50 | const event = req.body; |
| 51 | console.log(`Received ${event.RecordType} event`); |
| 52 | |
| 53 | res.sendStatus(200); |
| 54 | }); |
| 55 | ``` |
| 56 | |
| 57 | ### Handling Multiple Events |
| 58 | |
| 59 | ```javascript |
| 60 | // Postmark sends one event per request (not batched) |
| 61 | app.post('/webhooks/postmark', express.json(), (req, res) => { |
| 62 | const event = req.body; |
| 63 | |
| 64 | switch (event.RecordType) { |
| 65 | case 'Bounce': |
| 66 | console.log(`Bounce: ${event.Email} - ${event.Type} - ${event.Description}`); |
| 67 | // Update contact as undeliverable |
| 68 | break; |
| 69 | |
| 70 | case 'SpamComplaint': |
| 71 | console.log(`Spam complaint: ${event.Email}`); |
| 72 | // Remove from mailing list |
| 73 | break; |
| 74 | |
| 75 | case 'Open': |
| 76 | console.log(`Email opened: ${event.Email} at ${event.ReceivedAt}`); |
| 77 | // Track engagement |
| 78 | break; |
| 79 | |
| 80 | case 'Click': |
| 81 | console.log(`Link clicked: ${event.Email} - ${event.OriginalLink}`); |
| 82 | // Track click-through rate |
| 83 | break; |
| 84 | |
| 85 | case 'Delivery': |
| 86 | console.log(`Delivered: ${event.Email} at ${event.DeliveredAt}`); |
| 87 | // Confirm delivery |
| 88 | break; |
| 89 | |
| 90 | case 'SubscriptionChange': |
| 91 | console.log(`Subscription change: ${event.Email} - ${event.ChangedAt}`); |
| 92 | // Update subscription preferences |
| 93 | break; |
| 94 | |
| 95 | case 'Inbound': |
| 96 | console.log(`Inbound email from: ${event.Email} - Subject: ${event.Subject}`); |
| 97 | // Process incoming email |
| 98 | break; |
| 99 | |
| 100 | case 'SMTP API Error': |
| 101 | console.log(`SMTP API error: ${event.Email} - ${event.Error}`); |
| 102 | // Handle API error, maybe retry |
| 103 | break; |
| 104 | |
| 105 | default: |
| 106 | console.log(`Unknown event type: ${event.RecordType}`); |
| 107 | } |
| 108 | |
| 109 | res.sendStatus(200); |
| 110 | }); |
| 111 | ``` |
| 112 | |
| 113 | ## Common Event Types |
| 114 | |
| 115 | | Event | RecordType | Description | Key Fields | |
| 116 | |-------|------------|-------------|------------| |
| 117 | | Bounce | `Bounce` | Hard/soft bounce or blocked email | Email, Type, TypeCode, Description | |
| 118 | | Spam Complaint | `SpamComplaint` | Recipient marked as spam | Email, BouncedAt | |
| 119 | | Open | `Open` | Email opened (requires open tracking) | Email, ReceivedAt, Platform, UserAgent | |
| 120 | | Click | `Click` | Link clicked (requires click tracking) | Email, ClickedAt, OriginalLink | |
| 121 | | Delivery | `Delivery` | Successfully delivered | Email, DeliveredAt, Details | |
| 122 | | Subscription Change | `SubscriptionChange` | Unsubscribe/resubscribe | Email, ChangedAt, SuppressionReason | |
| 123 | | Inbound | `Inbound` | Incoming email received | Email, FromFull, Subject, TextBody, HtmlBody | |
| 124 | | SMTP API Error | `SMTP API Error` | SMTP API call failed | Email, Error, ErrorCode, MessageID | |
| 125 | |
| 126 | ## Environment Variables |
| 127 | |
| 128 | ```bash |
| 129 | # For token-based authentication |
| 130 | POSTMARK_WEBHOOK_TOKEN="your-secret-token-here" |
| 131 | |
| 132 | # For basic auth (if not using URL-embedded credentials) |
| 133 | WEBHOOK_USERNAME="your-username" |
| 134 | WEBHOOK_PASSWORD="your-password" |
| 135 | ``` |
| 136 | |
| 137 | ## Security Best Practices |
| 138 | |
| 139 | 1. **Always use HTTPS** - Never configure webhooks with HTTP URLs |
| 140 | 2. **Use strong credentials** - Generate long, random tokens or passwords |
| 141 | 3. **Validate payload structure** - Check for expected fields before processing |
| 142 | 4. **Implement IP allowlisting** - Postmark publishes their IP ranges |
| 143 | 5. **Consider using a webhook gateway** - Like Hookdeck for additional security layers |
| 144 | |
| 145 | ## Local Development |
| 146 | |
| 147 | For local webhook testi |