Postmark Integration
Receive and route Postmark webhooks for email delivery events, bounces, spam complaints, opens, and clicks.
Setup
1. Create a Source in Hookbase
Postmark does not natively sign webhook payloads with HMAC. Instead, you can secure your webhooks using one of two approaches:
Option A: Custom Header Token (Recommended)
Configure a shared secret token that Postmark sends as a custom header with each webhook request:
curl -X POST https://api.hookbase.app/api/sources \
-H "Authorization: Bearer whr_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"name": "Postmark Production",
"slug": "postmark",
"verificationConfig": {
"type": "hmac",
"secret": "your-postmark-webhook-token",
"algorithm": "sha256",
"header": "X-Postmark-Token",
"encoding": "hex"
}
}'Option B: Basic Auth on the Ingest URL
Embed credentials directly in the webhook URL. This approach requires no additional configuration on the source:
curl -X POST https://api.hookbase.app/api/sources \
-H "Authorization: Bearer whr_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"name": "Postmark Production",
"slug": "postmark"
}'Then use your webhook URL with Basic Auth credentials embedded:
https://username:[email protected]/ingest/{orgSlug}/postmarkSave your webhook URL (without Basic Auth, for reference):
https://api.hookbase.app/ingest/{orgSlug}/postmark2. Configure Postmark Webhooks
- Go to Postmark and sign in
- Navigate to Servers → select your server → Settings → Webhooks
- Click Add webhook
- Enter your Hookbase webhook URL (with Basic Auth credentials if using Option B)
- Select the event types you want to receive (Delivery, Bounce, Spam Complaint, Open, Click, Subscription Change)
- If using Option A, add a custom HTTP header: set the header name to
X-Postmark-Tokenand the value to your shared secret - Click Save webhook
TIP
Postmark allows you to configure separate webhook URLs for different event types. You can create multiple Hookbase sources (e.g., postmark-bounces, postmark-deliveries) to route different event types independently.
3. Create Routes
# Create destination for your email event handler
curl -X POST https://api.hookbase.app/api/destinations \
-H "Authorization: Bearer whr_your_api_key" \
-H "Content-Type: application/json" \
-d '{"name": "Email Service", "url": "https://api.myapp.com/webhooks/postmark"}'
# Create route
curl -X POST https://api.hookbase.app/api/routes \
-H "Authorization: Bearer whr_your_api_key" \
-H "Content-Type: application/json" \
-d '{"name": "Postmark to Email Service", "sourceId": "src_...", "destinationIds": ["dst_..."]}'Signature Verification
Postmark does not provide built-in HMAC signature verification like Stripe or GitHub. To secure your webhooks, use one of the following strategies:
Custom Header Token
When creating your webhook in Postmark, add a custom HTTP header (e.g., X-Postmark-Token) with a shared secret value. Configure your Hookbase source to validate this header:
{
"verificationConfig": {
"type": "hmac",
"secret": "your-postmark-webhook-token",
"algorithm": "sha256",
"header": "X-Postmark-Token",
"encoding": "hex"
}
}Hookbase will reject any request that does not include the correct token in the specified header.
Basic Auth
Embed a username and password in the webhook URL you give to Postmark:
https://hookbase-user:s3cureP@[email protected]/ingest/{orgSlug}/postmarkHookbase validates the credentials before processing the webhook. This approach is simple but means the credentials are visible in Postmark's webhook configuration UI.
WARNING
Always use HTTPS when using Basic Auth to prevent credentials from being transmitted in plain text.
IP Allowlisting
As an additional layer of security, you can restrict inbound webhooks to Postmark's IP ranges. Check Postmark's documentation for their current IP addresses.
Common Events
Delivery
Triggered when an email is successfully delivered to the recipient's mail server.
{
"RecordType": "Delivery",
"ServerID": 23,
"MessageStream": "outbound",
"MessageID": "883953f4-6105-42a2-a16a-77a8eac79483",
"Recipient": "[email protected]",
"Tag": "welcome-email",
"DeliveredAt": "2026-03-07T16:33:54.9070259Z",
"Details": "Test delivery webhook details",
"Metadata": {
"user_id": "usr_12345",
"campaign": "onboarding"
}
}Bounce
Triggered when an email bounces. Includes the bounce type and description.
{
"RecordType": "Bounce",
"MessageStream": "outbound",
"ID": 4323372036854775807,
"Type": "HardBounce",
"TypeCode": 1,
"Name": "Hard bounce",
"Tag": "welcome-email",
"MessageID": "883953f4-6105-42a2-a16a-77a8eac79483",
"ServerID": 23,
"Description": "The server was unable to deliver your message (ex: unknown user, mailbox not found).",
"Details": "smtp;550 5.1.1 The email account that you tried to reach does not exist.",
"Email": "[email protected]",
"From": "[email protected]",
"BouncedAt": "2026-03-07T16:33:54.9070259Z",
"DumpAvailable": true,
"Inactive": true,
"CanActivate": true,
"Subject": "Welcome to MyApp"
}SpamComplaint
Triggered when a recipient marks your email as spam.
{
"RecordType": "SpamComplaint",
"ID": 42,
"Type": "SpamComplaint",
"TypeCode": 512,
"Name": "Spam complaint",
"Tag": "marketing",
"MessageID": "883953f4-6105-42a2-a16a-77a8eac79483",
"ServerID": 23,
"Description": "",
"Email": "[email protected]",
"From": "[email protected]",
"BouncedAt": "2026-03-07T14:22:01.0000000Z",
"Subject": "Weekly Newsletter",
"Inactive": true,
"CanActivate": false
}Open
Triggered when a recipient opens an email. Includes geo and client info.
{
"RecordType": "Open",
"FirstOpen": true,
"Client": {
"Name": "Chrome Mobile",
"Company": "Google",
"Family": "Chrome"
},
"OS": {
"Name": "Android",
"Company": "Google",
"Family": "Android"
},
"Platform": "Mobile",
"UserAgent": "Mozilla/5.0 (Linux; Android 14) AppleWebKit/537.36",
"ReadSeconds": 5,
"Geo": {
"CountryISOCode": "US",
"Country": "United States",
"RegionISOCode": "CA",
"Region": "California",
"City": "San Francisco",
"Zip": "94107",
"Coords": "37.7749,-122.4194",
"IP": "203.0.113.1"
},
"MessageID": "883953f4-6105-42a2-a16a-77a8eac79483",
"MessageStream": "outbound",
"ReceivedAt": "2026-03-07T17:01:23.0000000Z",
"Tag": "welcome-email",
"Recipient": "[email protected]",
"Metadata": {
"user_id": "usr_12345"
}
}Click
Triggered when a recipient clicks a link in your email.
{
"RecordType": "Click",
"ClickLocation": "HTML",
"Client": {
"Name": "Chrome",
"Company": "Google",
"Family": "Chrome"
},
"OS": {
"Name": "Windows",
"Company": "Microsoft",
"Family": "Windows"
},
"Platform": "Desktop",
"UserAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
"OriginalLink": "https://myapp.com/activate?token=abc123",
"Geo": {
"CountryISOCode": "US",
"Country": "United States",
"RegionISOCode": "NY",
"Region": "New York",
"City": "New York",
"Zip": "10001",
"Coords": "40.7128,-74.0060",
"IP": "198.51.100.42"
},
"MessageID": "883953f4-6105-42a2-a16a-77a8eac79483",
"MessageStream": "outbound",
"ReceivedAt": "2026-03-07T17:05:44.0000000Z",
"Tag": "welcome-email",
"Recipient": "[email protected]",
"Metadata": {
"user_id": "usr_12345"
}
}SubscriptionChange
Triggered when a recipient changes their subscription preferences.
{
"RecordType": "SubscriptionChange",
"ChangedAt": "2026-03-07T18:00:00.0000000Z",
"Recipient": "[email protected]",
"Origin": "Recipient",
"SuppressSending": true,
"Tag": "marketing",
"MessageStream": "broadcast",
"ServerID": 23,
"MessageID": "883953f4-6105-42a2-a16a-77a8eac79483",
"Metadata": {
"user_id": "usr_12345"
}
}Postmark Event Types
| Event Type | RecordType | Description |
|---|---|---|
| Delivery | Delivery | Email successfully delivered to recipient's server |
| Bounce | Bounce | Email bounced (hard or soft) |
| Spam Complaint | SpamComplaint | Recipient reported email as spam |
| Open | Open | Recipient opened the email |
| Click | Click | Recipient clicked a link in the email |
| Subscription Change | SubscriptionChange | Recipient changed subscription preferences |
Bounce Types
| Type | TypeCode | Description |
|---|---|---|
HardBounce | 1 | Permanent delivery failure (e.g., invalid address) |
SoftBounce | 4096 | Temporary delivery failure (e.g., mailbox full) |
Transient | 2 | Intermittent issue, may resolve on retry |
Blocked | 16 | Blocked by recipient server |
AutoResponder | 32 | Auto-reply message |
AddressChange | 512 | Address has changed |
DnsError | 8 | DNS resolution failure |
Transform Examples
Slack Alert for Bounces
function transform(payload) {
const isSoftBounce = payload.Type === 'SoftBounce';
const color = isSoftBounce ? 'warning' : 'danger';
const emoji = isSoftBounce ? ':warning:' : ':x:';
return {
blocks: [
{
type: "header",
text: {
type: "plain_text",
text: `${emoji} Email Bounce - ${payload.Type}`
}
},
{
type: "section",
fields: [
{
type: "mrkdwn",
text: `*Recipient:*\n${payload.Email}`
},
{
type: "mrkdwn",
text: `*Bounce Type:*\n${payload.Name}`
},
{
type: "mrkdwn",
text: `*Subject:*\n${payload.Subject || 'N/A'}`
},
{
type: "mrkdwn",
text: `*From:*\n${payload.From}`
}
]
},
{
type: "section",
text: {
type: "mrkdwn",
text: `*Details:*\n${payload.Description}`
}
},
{
type: "context",
elements: [
{
type: "mrkdwn",
text: `Message ID: ${payload.MessageID} | Bounced at: ${payload.BouncedAt}`
}
]
}
]
};
}Database Format
function transform(payload) {
const recordType = payload.RecordType;
const base = {
postmark_message_id: payload.MessageID,
record_type: recordType,
recipient: payload.Recipient || payload.Email,
tag: payload.Tag || null,
message_stream: payload.MessageStream || 'outbound',
server_id: payload.ServerID,
metadata: payload.Metadata || {},
received_at: new Date().toISOString()
};
if (recordType === 'Delivery') {
return {
...base,
delivered_at: payload.DeliveredAt
};
}
if (recordType === 'Bounce') {
return {
...base,
bounce_type: payload.Type,
bounce_type_code: payload.TypeCode,
bounce_description: payload.Description,
bounce_details: payload.Details,
from_address: payload.From,
subject: payload.Subject,
is_inactive: payload.Inactive,
bounced_at: payload.BouncedAt
};
}
if (recordType === 'SpamComplaint') {
return {
...base,
from_address: payload.From,
subject: payload.Subject,
is_inactive: payload.Inactive,
complained_at: payload.BouncedAt
};
}
if (recordType === 'Open') {
return {
...base,
first_open: payload.FirstOpen,
platform: payload.Platform,
os: payload.OS?.Name,
client: payload.Client?.Name,
country: payload.Geo?.Country,
city: payload.Geo?.City,
opened_at: payload.ReceivedAt
};
}
if (recordType === 'Click') {
return {
...base,
original_link: payload.OriginalLink,
click_location: payload.ClickLocation,
platform: payload.Platform,
os: payload.OS?.Name,
client: payload.Client?.Name,
country: payload.Geo?.Country,
city: payload.Geo?.City,
clicked_at: payload.ReceivedAt
};
}
return base;
}Spam Complaint Alert
function transform(payload) {
return {
channel: "#email-alerts",
username: "Postmark Bot",
icon_emoji: ":rotating_light:",
attachments: [{
color: "danger",
title: "Spam Complaint Received",
fields: [
{ title: "Recipient", value: payload.Email, short: true },
{ title: "From", value: payload.From, short: true },
{ title: "Subject", value: payload.Subject || "N/A", short: true },
{ title: "Tag", value: payload.Tag || "N/A", short: true }
],
footer: `Message ID: ${payload.MessageID}`,
ts: Math.floor(Date.now() / 1000)
}]
};
}Filter Examples
Bounces Only
{
"name": "Bounces Only",
"logic": "and",
"conditions": [
{
"field": "RecordType",
"operator": "equals",
"value": "Bounce"
}
]
}Hard Bounces Only
{
"name": "Hard Bounces",
"logic": "and",
"conditions": [
{
"field": "RecordType",
"operator": "equals",
"value": "Bounce"
},
{
"field": "Type",
"operator": "equals",
"value": "HardBounce"
}
]
}Delivery and Bounce Events
{
"name": "Delivery Status Events",
"logic": "or",
"conditions": [
{
"field": "RecordType",
"operator": "equals",
"value": "Delivery"
},
{
"field": "RecordType",
"operator": "equals",
"value": "Bounce"
}
]
}Engagement Events (Opens and Clicks)
{
"name": "Engagement Events",
"logic": "or",
"conditions": [
{
"field": "RecordType",
"operator": "equals",
"value": "Open"
},
{
"field": "RecordType",
"operator": "equals",
"value": "Click"
}
]
}Specific Tag
{
"name": "Welcome Email Events",
"logic": "and",
"conditions": [
{
"field": "Tag",
"operator": "equals",
"value": "welcome-email"
}
]
}Headers
Postmark sends these headers with webhook requests:
| Header | Description |
|---|---|
Content-Type | Always application/json |
User-Agent | Postmark user agent string |
X-PM-Webhook-EventType | The event type (e.g., Bounce, Delivery) |
X-PM-Webhook-TraceID | Unique ID for the webhook delivery |
X-Postmark-Token | Custom header token (if configured) |
Testing
Using Postmark's Test Webhook
Postmark provides a built-in webhook test tool:
- Go to Servers → your server → Settings → Webhooks
- Click on your webhook
- Use the Send test button to send a sample payload for each event type
Manual Test
Send a test Postmark-style payload to your Hookbase source:
curl -X POST https://api.hookbase.app/ingest/{orgSlug}/postmark \
-H "Content-Type: application/json" \
-H "X-Postmark-Token: your-postmark-webhook-token" \
-d '{
"RecordType": "Delivery",
"ServerID": 23,
"MessageStream": "outbound",
"MessageID": "test-message-id",
"Recipient": "[email protected]",
"Tag": "test",
"DeliveredAt": "2026-03-07T16:33:54Z",
"Details": "Test delivery",
"Metadata": {}
}'Best Practices
Secure your webhooks: Since Postmark lacks native HMAC signing, always use a custom header token or Basic Auth to verify webhook authenticity
Handle all bounce types: Distinguish between hard and soft bounces. Hard bounces should suppress future sends; soft bounces may be temporary
Act on spam complaints immediately: Remove complainers from your mailing lists to protect your sender reputation
Use separate sources per stream: If you use multiple Postmark message streams (transactional, broadcast), create separate Hookbase sources for each
Track engagement metrics: Use Open and Click events to build engagement profiles, but remember that open tracking is not 100% accurate due to email client privacy features
Store MessageID: Log Postmark's
MessageIDfor cross-referencing events with your sent email recordsMonitor inactive recipients: When
Inactiveistrueon a bounce or complaint, Postmark has suppressed sending to that address
Troubleshooting
Webhook Not Triggering
- Verify the webhook URL is correct in Servers → Settings → Webhooks
- Ensure the correct event types are checked (Delivery, Bounce, etc.)
- Check that your server is actively sending emails that would trigger events
- Use Postmark's Send test button to verify connectivity
Authentication Failing
- If using Basic Auth, verify the username and password in the URL are correct and URL-encoded
- If using a custom header token, confirm the header name and value match between Postmark and your Hookbase source configuration
- Check that the
verificationConfigsecret matches the token value exactly
Missing Events
- Confirm the specific event type is enabled on your Postmark webhook
- Postmark requires separate webhook configurations per event type in some setups
- Check Postmark's Activity tab for delivery logs and event history
- Verify filters in Hookbase are not excluding expected events
Bounce Events Not Appearing
- Bounces only fire for actual delivery failures -- test with a known-invalid address
- Check if the email was suppressed before sending (Postmark skips sending to suppressed addresses)
- Review the Bounces tab in Postmark's dashboard for bounce records
Duplicate Events
- Postmark may retry webhook deliveries if it does not receive a 2xx response
- Use the
MessageIDfield combined withRecordTypefor deduplication - Enable deduplication on your Hookbase source:
curl -X PATCH .../sources/src_postmark \
-d '{"dedupEnabled": true, "dedupStrategy": "provider_id"}'