Skip to content

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:

bash
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:

bash
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}/postmark

Save your webhook URL (without Basic Auth, for reference):

https://api.hookbase.app/ingest/{orgSlug}/postmark

2. Configure Postmark Webhooks

  1. Go to Postmark and sign in
  2. Navigate to Servers → select your server → SettingsWebhooks
  3. Click Add webhook
  4. Enter your Hookbase webhook URL (with Basic Auth credentials if using Option B)
  5. Select the event types you want to receive (Delivery, Bounce, Spam Complaint, Open, Click, Subscription Change)
  6. If using Option A, add a custom HTTP header: set the header name to X-Postmark-Token and the value to your shared secret
  7. 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

bash
# 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:

json
{
  "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}/postmark

Hookbase 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.

json
{
  "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.

json
{
  "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.

json
{
  "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.

json
{
  "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.

json
{
  "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.

json
{
  "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 TypeRecordTypeDescription
DeliveryDeliveryEmail successfully delivered to recipient's server
BounceBounceEmail bounced (hard or soft)
Spam ComplaintSpamComplaintRecipient reported email as spam
OpenOpenRecipient opened the email
ClickClickRecipient clicked a link in the email
Subscription ChangeSubscriptionChangeRecipient changed subscription preferences

Bounce Types

TypeTypeCodeDescription
HardBounce1Permanent delivery failure (e.g., invalid address)
SoftBounce4096Temporary delivery failure (e.g., mailbox full)
Transient2Intermittent issue, may resolve on retry
Blocked16Blocked by recipient server
AutoResponder32Auto-reply message
AddressChange512Address has changed
DnsError8DNS resolution failure

Transform Examples

Slack Alert for Bounces

javascript
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

javascript
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

javascript
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

json
{
  "name": "Bounces Only",
  "logic": "and",
  "conditions": [
    {
      "field": "RecordType",
      "operator": "equals",
      "value": "Bounce"
    }
  ]
}

Hard Bounces Only

json
{
  "name": "Hard Bounces",
  "logic": "and",
  "conditions": [
    {
      "field": "RecordType",
      "operator": "equals",
      "value": "Bounce"
    },
    {
      "field": "Type",
      "operator": "equals",
      "value": "HardBounce"
    }
  ]
}

Delivery and Bounce Events

json
{
  "name": "Delivery Status Events",
  "logic": "or",
  "conditions": [
    {
      "field": "RecordType",
      "operator": "equals",
      "value": "Delivery"
    },
    {
      "field": "RecordType",
      "operator": "equals",
      "value": "Bounce"
    }
  ]
}

Engagement Events (Opens and Clicks)

json
{
  "name": "Engagement Events",
  "logic": "or",
  "conditions": [
    {
      "field": "RecordType",
      "operator": "equals",
      "value": "Open"
    },
    {
      "field": "RecordType",
      "operator": "equals",
      "value": "Click"
    }
  ]
}

Specific Tag

json
{
  "name": "Welcome Email Events",
  "logic": "and",
  "conditions": [
    {
      "field": "Tag",
      "operator": "equals",
      "value": "welcome-email"
    }
  ]
}

Headers

Postmark sends these headers with webhook requests:

HeaderDescription
Content-TypeAlways application/json
User-AgentPostmark user agent string
X-PM-Webhook-EventTypeThe event type (e.g., Bounce, Delivery)
X-PM-Webhook-TraceIDUnique ID for the webhook delivery
X-Postmark-TokenCustom header token (if configured)

Testing

Using Postmark's Test Webhook

Postmark provides a built-in webhook test tool:

  1. Go to Servers → your server → SettingsWebhooks
  2. Click on your webhook
  3. 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:

bash
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

  1. Secure your webhooks: Since Postmark lacks native HMAC signing, always use a custom header token or Basic Auth to verify webhook authenticity

  2. Handle all bounce types: Distinguish between hard and soft bounces. Hard bounces should suppress future sends; soft bounces may be temporary

  3. Act on spam complaints immediately: Remove complainers from your mailing lists to protect your sender reputation

  4. Use separate sources per stream: If you use multiple Postmark message streams (transactional, broadcast), create separate Hookbase sources for each

  5. 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

  6. Store MessageID: Log Postmark's MessageID for cross-referencing events with your sent email records

  7. Monitor inactive recipients: When Inactive is true on a bounce or complaint, Postmark has suppressed sending to that address

Troubleshooting

Webhook Not Triggering

  1. Verify the webhook URL is correct in ServersSettingsWebhooks
  2. Ensure the correct event types are checked (Delivery, Bounce, etc.)
  3. Check that your server is actively sending emails that would trigger events
  4. Use Postmark's Send test button to verify connectivity

Authentication Failing

  1. If using Basic Auth, verify the username and password in the URL are correct and URL-encoded
  2. If using a custom header token, confirm the header name and value match between Postmark and your Hookbase source configuration
  3. Check that the verificationConfig secret matches the token value exactly

Missing Events

  1. Confirm the specific event type is enabled on your Postmark webhook
  2. Postmark requires separate webhook configurations per event type in some setups
  3. Check Postmark's Activity tab for delivery logs and event history
  4. Verify filters in Hookbase are not excluding expected events

Bounce Events Not Appearing

  1. Bounces only fire for actual delivery failures -- test with a known-invalid address
  2. Check if the email was suppressed before sending (Postmark skips sending to suppressed addresses)
  3. Review the Bounces tab in Postmark's dashboard for bounce records

Duplicate Events

  1. Postmark may retry webhook deliveries if it does not receive a 2xx response
  2. Use the MessageID field combined with RecordType for deduplication
  3. Enable deduplication on your Hookbase source:
bash
curl -X PATCH .../sources/src_postmark \
  -d '{"dedupEnabled": true, "dedupStrategy": "provider_id"}'

Released under the MIT License.