Guide to Postmark Webhooks: Features and Best Practices
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
| Feature | Details |
|---|---|
| Webhook configuration | Postmark UI, Webhooks API |
| Authentication | Basic HTTP Auth, Custom HTTP Headers |
| Signature verification | Not natively supported |
| Timeout | Not publicly documented |
| Retry logic | Up to 10 retries with increasing intervals |
| Event selection | Granular per-event type control |
| Manual retry | Available for inbound webhooks only |
| Browsable log | Limited; 45 days of message activity in UI |
| Maximum webhook URLs | Up to 10 per message stream |
Supported event types
Postmark webhooks can notify you of the following email events:
| Event Type | Description |
|---|---|
| Bounce | Email bounced (hard bounce, soft bounce, DNS error, etc.) |
| Delivery | Email successfully delivered to the receiving server |
| Open | Recipient opened the email (requires open tracking enabled) |
| Click | Recipient clicked a tracked link in the email |
| Spam Complaint | Recipient marked the email as spam |
| Subscription Change | Recipient unsubscribed or resubscribed |
| Inbound | Email 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
| Field | Description |
|---|---|
| RecordType | The type of webhook event (Bounce, Delivery, Open, Click, etc.) |
| MessageStream | The message stream this event belongs to (outbound, broadcast, or custom) |
| ID | Unique identifier for the event (int64 value) |
| MessageID | UUID of the original message |
| Metadata | Custom key-value pairs you included when sending the email |
| Tag | Optional tag you assigned to the message |
| The recipient's email address | |
| BouncedAt/DeliveredAt/ReceivedAt | ISO 8601 timestamp of when the event occurred |
Authentication options
Postmark webhooks support multiple methods to secure your endpoints:
| Method | Configuration |
|---|---|
| Basic HTTP Auth | Username and password embedded in the webhook URL |
| Custom HTTP Headers | Up to 10 custom headers per webhook configuration |
| IP Allowlisting | Configure firewall to accept only Postmark's IP ranges |
| HTTPS | Strongly 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
- Log into Postmark and select your Server
- Choose the Message Stream you want to configure
- Navigate to the Webhooks tab
- Click Add webhook
- Enter your webhook URL in the Webhook URL field
- Select which events to enable (Bounce, Delivery, Open, Click, Spam Complaint, Subscription Change)
- Optionally configure:
- HTTP Auth: Username and password for Basic Authentication
- HTTP Headers: Custom headers to include with each request
- 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:
- Select your Server in Postmark
- Go to the Inbound message stream
- Click Settings
- Enter your webhook URL in the Webhook field
- 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.comor@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
5xxerrors for temporary issues (which trigger retries) rather than403 - Implement proper error handling that distinguishes between authorisation failures and transient errors
- Monitor for unexpected
403responses 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:
- Create a temporary URL at Hookdeck Request Inspector.
- Configure it as your webhook URL in Postmark
- Trigger real events (send a test email, click a link, etc.)
- 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.comfor a hard bounce - Send to
test@devnull.postmarkapp.comfor 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.