Author picture Gareth Wilson

Guide to Postmark Webhooks: Features and Best Practices

Published


Postmark has established itself as a leading transactional email service, trusted by developers who prioritize deliverability and simplicity. Beyond sending emails, Postmark's webhook system enables real-time notifications about email events, allowing applications to respond immediately to bounces, deliveries, opens, clicks, and more.

This guide covers everything you need to know about Postmark webhooks: their features, how to configure them, best practices for production deployments, and the common pain points developers face along with solutions to address them.

What are Postmark webhooks?

Postmark webhooks are HTTP POST requests that Postmark sends to your application's API when email events occur. Instead of polling Postmark's API to check for updates, your application receives instant notifications in JSON format as soon as events happen. This enables real-time processing of bounces, delivery confirmations, engagement tracking, and inbound email handling.

Webhooks in Postmark exist in two primary contexts:

  • Outbound Message Stream Webhooks - Notifications for events related to emails you send (bounces, deliveries, opens, clicks, spam complaints, subscription changes)
  • Inbound Message Stream Webhooks - Notifications when Postmark receives and parses emails sent to your inbound addresses

This guide covers both webhook types, with a focus on outbound webhooks as they're most commonly implemented.

Postmark webhook features

FeatureDetails
Webhook configurationPostmark UI, Webhooks API
AuthenticationBasic HTTP Auth, Custom HTTP Headers
Signature verificationNot natively supported
TimeoutNot publicly documented
Retry logicUp to 10 retries with increasing intervals
Event selectionGranular per-event type control
Manual retryAvailable for inbound webhooks only
Browsable logLimited; 45 days of message activity in UI
Maximum webhook URLsUp to 10 per message stream

Supported event types

Postmark webhooks can notify you of the following email events:

Event TypeDescription
BounceEmail bounced (hard bounce, soft bounce, DNS error, etc.)
DeliveryEmail successfully delivered to the receiving server
OpenRecipient opened the email (requires open tracking enabled)
ClickRecipient clicked a tracked link in the email
Spam ComplaintRecipient marked the email as spam
Subscription ChangeRecipient unsubscribed or resubscribed
InboundEmail received at your Postmark inbound address

Each webhook delivery includes detailed information about the event, allowing your endpoint to take appropriate action.

Webhook payload structure

When Postmark sends a webhook notification, it delivers a JSON payload specific to the event type. Here's an example bounce webhook payload:

{
  "RecordType": "Bounce",
  "MessageStream": "outbound",
  "ID": 4323372036854775807,
  "Type": "HardBounce",
  "TypeCode": 1,
  "Name": "Hard bounce",
  "Tag": "welcome-email",
  "MessageID": "883953f4-6105-42a2-a16a-77a8eac79483",
  "Metadata": {
    "user_id": "12345",
    "campaign": "onboarding"
  },
  "ServerID": 23,
  "Description": "The server was unable to deliver your message (ex: unknown user, mailbox not found).",
  "Details": "550 5.1.1 The email account does not exist",
  "Email": "john@example.com",
  "From": "sender@example.com",
  "BouncedAt": "2025-01-21T10:30:00.000Z",
  "DumpAvailable": true,
  "Inactive": true,
  "CanActivate": true,
  "Subject": "Welcome to our service",
  "Content": "<Full dump of bounce>"
}

Key payload fields

FieldDescription
RecordTypeThe type of webhook event (Bounce, Delivery, Open, Click, etc.)
MessageStreamThe message stream this event belongs to (outbound, broadcast, or custom)
IDUnique identifier for the event (int64 value)
MessageIDUUID of the original message
MetadataCustom key-value pairs you included when sending the email
TagOptional tag you assigned to the message
EmailThe recipient's email address
BouncedAt/DeliveredAt/ReceivedAtISO 8601 timestamp of when the event occurred

Authentication options

Postmark webhooks support multiple methods to secure your endpoints:

MethodConfiguration
Basic HTTP AuthUsername and password embedded in the webhook URL
Custom HTTP HeadersUp to 10 custom headers per webhook configuration
IP AllowlistingConfigure firewall to accept only Postmark's IP ranges
HTTPSStrongly recommended for all webhook URLs

To configure Basic Auth, format your webhook URL as:

https://<username>:<password>@example.com/webhooks/postmark

Postmark does not currently support HMAC signature verification for webhook payloads. Unlike some other providers, you cannot cryptographically verify that a webhook originated from Postmark using a shared secret.

Setting up Postmark webhooks

Via the Postmark UI

  1. Log into Postmark and select your Server
  2. Choose the Message Stream you want to configure
  3. Navigate to the Webhooks tab
  4. Click Add webhook
  5. Enter your webhook URL in the Webhook URL field
  6. Select which events to enable (Bounce, Delivery, Open, Click, Spam Complaint, Subscription Change)
  7. Optionally configure:
    • HTTP Auth: Username and password for Basic Authentication
    • HTTP Headers: Custom headers to include with each request
  8. Click Save webhook

Via the Webhooks API

curl -X POST "https://api.postmarkapp.com/webhooks" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -H "X-Postmark-Server-Token: your-server-token" \
  -d '{
    "Url": "https://your-endpoint.com/webhooks/postmark",
    "MessageStream": "outbound",
    "HttpAuth": {
      "Username": "webhook-user",
      "Password": "secure-password"
    },
    "HttpHeaders": [
      {
        "Name": "X-Custom-Header",
        "Value": "custom-value"
      }
    ],
    "Triggers": {
      "Open": {
        "Enabled": true,
        "PostFirstOpenOnly": false
      },
      "Click": {
        "Enabled": true
      },
      "Delivery": {
        "Enabled": true
      },
      "Bounce": {
        "Enabled": true,
        "IncludeContent": false
      },
      "SpamComplaint": {
        "Enabled": true,
        "IncludeContent": false
      },
      "SubscriptionChange": {
        "Enabled": true
      }
    }
  }'

Setting up inbound webhooks

Inbound webhooks are configured separately at the Server level:

  1. Select your Server in Postmark
  2. Go to the Inbound message stream
  3. Click Settings
  4. Enter your webhook URL in the Webhook field
  5. Save your changes

You can only have one inbound webhook URL per Inbound Message Stream.

Best practices when working with Postmark webhooks

Respond quickly and process asynchronously

Postmark expects a timely response from your webhook endpoint. Return a 200 OK status immediately upon receiving the webhook, then process the data asynchronously.

Use MessageID for idempotency

Each webhook includes a MessageID field that uniquely identifies the original message. Use this along with the RecordType to implement idempotent processing:

async function processWebhook(payload) {
  const idempotencyKey = `${payload.MessageID}-${payload.RecordType}`;

  // Check if already processed
  const exists = await redis.get(`processed:${idempotencyKey}`);
  if (exists) {
    console.log(`Webhook ${idempotencyKey} already processed, skipping`);
    return;
  }

  // Process the webhook
  await handleWebhookEvent(payload);

  // Mark as processed with TTL
  await redis.setex(`processed:${idempotencyKey}`, 86400, '1'); // 24 hour TTL
}

Validate the request origin

Since Postmark doesn't support HMAC signatures, implement alternative validation:

const POSTMARK_IPS = [
  // Get current IPs from Postmark's support documentation
  '50.31.156.0/24',
  '50.31.157.0/24',
  // ... add all Postmark IP ranges
];

function validatePostmarkRequest(req) {
  const clientIP = req.ip || req.connection.remoteAddress;

  // Validate IP is from Postmark
  const isValidIP = POSTMARK_IPS.some(range => ipRangeCheck(clientIP, range));

  // Validate Basic Auth if configured
  const authHeader = req.headers.authorization;
  const expectedAuth = Buffer.from(`${USERNAME}:${PASSWORD}`).toString('base64');
  const isValidAuth = authHeader === `Basic ${expectedAuth}`;

  return isValidIP && isValidAuth;
}

app.post('/webhooks/postmark', (req, res) => {
  if (!validatePostmarkRequest(req)) {
    return res.status(401).send('Unauthorized');
  }

  // Process webhook
  res.status(200).send('OK');
});

Handle different event types appropriately

Route webhooks to specific handlers based on the RecordType.

Leverage Metadata for context

Include custom metadata when sending emails to make webhook processing easier.

Postmark webhook limitations and pain points

No native HMAC signature verification

The Problem: Postmark webhooks don't include cryptographic signatures to verify that requests genuinely originated from Postmark. This makes it harder to definitively authenticate webhook requests.

Why It Happens: Postmark's webhook security model relies on Basic HTTP Auth and IP allowlisting rather than HMAC signatures. While these methods provide security, they don't offer the cryptographic verification that HMAC provides.

Workarounds:

  • Implement strict IP allowlisting using Postmark's published IP ranges
  • Use Basic HTTP Auth with strong, unique credentials per webhook
  • Deploy webhooks behind a WAF that can validate source IPs
  • Use a unique, unguessable webhook URL path as an additional layer

How Hookdeck Can Help: Hookdeck can add webhook verification at the gateway level, providing consistent security across all your webhook sources. For Postmark specifically, Hookdeck's IP verification and custom authentication rules can strengthen your security posture without modifying your endpoint code.

Limited webhook delivery visibility

The Problem: Postmark provides minimal visibility into webhook delivery status. While you can see 45 days of message activity, there's no dedicated dashboard showing webhook delivery success/failure rates, response times, or error details.

Why It Happens: Postmark's activity dashboard focuses on email events rather than webhook infrastructure health. Debugging webhook issues often requires checking your own server logs.

Workarounds:

  • Implement comprehensive logging in your webhook handler
  • Use external monitoring tools to track endpoint health
  • Set up alerts for webhook processing failures

How Hookdeck Can Help: Hookdeck provides a complete webhook delivery dashboard with real-time visibility into every webhook attempt, including response codes, latency metrics, and full request/response bodies. This eliminates the need to dig through server logs when troubleshooting.

No persistent event storage after retry exhaustion

The Problem: If your webhook endpoint is unavailable and all retry attempts fail (up to 10 retries over approximately 10.5 hours), the webhook data is lost. Postmark doesn't maintain a dead letter queue for failed webhooks.

Why It Happens: Postmark's retry system is designed for temporary outages, not extended downtime. Once retries are exhausted, there's no mechanism to recover missed webhooks.

Workarounds:

  • Ensure high availability for your webhook endpoints
  • Implement your own event logging by also polling the Messages API periodically
  • Use the Postmark Activity dashboard to manually identify gaps
  • For inbound webhooks only, you can retry failed messages from the UI

How Hookdeck Can Help: Hookdeck automatically stores all webhook events, including those that fail delivery. Failed webhooks remain available indefinitely for inspection and manual replay, ensuring you never lose critical email events during outages.

Inconsistent retry schedules across event types

The Problem: Different webhook types have different retry schedules. Bounce and Inbound webhooks retry up to 10 times over several hours, while Click, Open, Delivery, and Subscription webhooks have a shorter retry window (1 min, 5 mins, 15 mins only).

Why It Happens: Postmark prioritises critical events (bounces, inbound) with more aggressive retry behaviour, while treating engagement events (opens, clicks) as less time-sensitive.

Workarounds:

  • Ensure your endpoint has high availability, especially for engagement webhooks
  • Implement your own recovery mechanism using the Messages API for critical gaps
  • Set up monitoring to detect webhook failures quickly

How Hookdeck Can Help: Hookdeck provides configurable, consistent retry policies across all webhook types. You can define custom retry schedules with exponential backoff, ensuring all events, regardless of type, receive the same level of delivery assurance.

Single inbound webhook URL per stream

The Problem: Each Inbound Message Stream can only have one webhook URL. If you need to send inbound emails to multiple destinations (e.g., different microservices), you must build a routing layer.

Why It Happens: Unlike outbound webhooks (where you can configure up to 10 URLs), inbound webhooks are configured at the stream level with a single destination.

Workarounds:

  • Build an internal fan-out service that receives the webhook and distributes to multiple destinations
  • Use a message queue to decouple receipt from processing
  • Create multiple inbound streams if you need different routing (though this requires different inbound addresses)

How Hookdeck Can Help: Hookdeck supports multi-destination routing out of the box. A single incoming webhook can be fanned out to multiple destinations with independent retry policies, transformations, and filtering rules.

No built-in webhook testing environment

The Problem: While Postmark provides curl examples for testing, there's no sandbox or test mode that simulates all webhook scenarios. Testing bounces in production can affect your sender reputation.

Why It Happens: Postmark does offer a black hole domain for generating fake bounces safely, but testing other scenarios (delivery, opens, clicks, spam complaints) requires sending actual emails.

Workarounds:

  • Use Postmark's black hole email domain for bounce testing (addresses ending in @blackhole.postmarkapp.com or @devnull.postmarkapp.com)
  • Create a staging server with test email addresses you control
  • Use the curl examples to simulate payloads during development

How Hookdeck Can Help: Hookdeck's Console provides a request inspector where you can capture, view, and replay webhooks. You can also manually create test events to simulate any scenario without affecting your production Postmark account.

403 responses permanently stop retries

The Problem: If your endpoint returns a 403 Forbidden response, Postmark immediately stops all retry attempts. This can cause webhooks to be lost if a temporary authorisation issue occurs.

Why It Happens: Postmark interprets 403 as a deliberate rejection, assuming your endpoint is intentionally refusing the webhook. This prevents wasting resources on endpoints that will never accept the request.

Workarounds:

  • Ensure your endpoint returns 5xx errors for temporary issues (which trigger retries) rather than 403
  • Implement proper error handling that distinguishes between authorisation failures and transient errors
  • Monitor for unexpected 403 responses in your logs

How Hookdeck Can Help: Hookdeck acts as a stable intermediary, accepting all webhooks reliably. Even if your downstream endpoint returns 403, Hookdeck stores the event for later inspection and can retry with different configurations once the issue is resolved.

Testing Postmark webhooks

Use the built-in test feature

When configuring a webhook in the Postmark UI, you can send test payloads to verify connectivity. However, test payloads may differ slightly from production data.

Test with curl

Postmark provides curl examples for each webhook type:

curl https://your-endpoint.com/webhooks/postmark \
  -X POST \
  -H "Content-Type: application/json" \
  -d '{
    "RecordType": "Bounce",
    "ID": 4323372036854775807,
    "Type": "HardBounce",
    "TypeCode": 1,
    "Name": "Hard bounce",
    "MessageID": "883953f4-6105-42a2-a16a-77a8eac79483",
    "ServerID": 23,
    "Description": "The server was unable to deliver your message.",
    "Email": "test@example.com",
    "From": "sender@example.com",
    "BouncedAt": "2025-01-21T10:30:00.000Z",
    "Inactive": true,
    "CanActivate": true,
    "Subject": "Test subject"
  }'

Use Hookdeck Console for inspection

Before building your handler, inspect real Postmark payloads:

  1. Create a temporary URL at Hookdeck Request Inspector.
  2. Configure it as your webhook URL in Postmark
  3. Trigger real events (send a test email, click a link, etc.)
  4. Inspect the payload structure and headers

Generate test bounces safely

Use Postmark's black hole domains to test bounce handling without affecting your sender reputation:

  • Send to test@blackhole.postmarkapp.com for a hard bounce
  • Send to test@devnull.postmarkapp.com for message disposal without bounce

Conclusion

Postmark webhooks provide a reliable foundation for building real-time email event processing into your applications. The comprehensive event types, from bounces to clicks to inbound parsing, enable sophisticated email automation and monitoring.

However, limitations around signature verification, delivery visibility, event persistence, and retry flexibility mean production deployments require careful planning. Implementing proper authentication, idempotent processing, and robust error handling will address most common challenges.

For applications where webhook reliability is mission-critical, such as transactional email monitoring, automated bounce handling, or inbound email processing pipelines, webhook infrastructure like Hookdeck can address Postmark's limitations. Hookdeck provides cryptographic verification alternatives, comprehensive delivery monitoring, persistent event storage, and flexible retry policies, ensuring you never miss a critical email event without building and maintaining custom infrastructure.

Additional resources


Author picture

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.