A webhook is the act of Postmark making an HTTP POST request to your application’s API when an event occurs. This way Postmark is able to immediately notify you when an event occurs and your application doesn’t have to perform complicated polling of Postmark’s API to determine if something new occurred.
Note: The datetimes within webhook data will be in ISO 8601 format.
When you create or edit a webhook, Postmark tests your endpoint for each event type you've enabled and checks that it responds with a 200. This confirms your endpoint is reachable and handling events before Postmark starts sending real ones.
If an event fails the check, you'll see which event failed and the response Postmark received, so you can fix it before you depend on it.
Verification isn't only a one-time check. If an event type later fails every delivery attempt, we'll email you a warning. Postmark keeps sending events while you fix your endpoint. If nothing gets through within 48 hours, we disable that event type until you fix your endpoint and verify your webhook again. Because this is tracked per event type, a problem with one type (say, Bounce) doesn't affect your others (say, Delivery or Open). See Webhook health notifications.
You can verify a webhook in three ways:
See this Allowlisting Postmark IP ranges article for Postmark IP ranges.
Note: This applies to outbound webhooks — delivery, bounce, open, click, spam complaint, and subscription change events. Inbound webhooks use separate infrastructure and aren't verified this way.
For Postmark to communicate with your application, you will need a publicly accessible URL. You’ll want to protect this URL so that malicious actors cannot manipulate your data.
If you’re using a firewall, you can configure it to only allow requests from the IP range that Postmark uses. You can find the IPs that Postmark uses for webhook requests on our support page. The origin IP address can change for each attempt.
Another easy way to protect your API is through basic HTTP authentication. Almost all web servers can be configured to require a user name and password for access to a URL. You can configure your webhook URL with basic HTTP authentication by adding the user name and password to the URL https://example.com/webhook in the following format and setting the result as the webhook URL:
https://<username>:<password>@example.com/webhook
To ensure your data is encrypted, we highly recommend using HTTPS (SSL) for your webhook URL.
Event Handler
export async function handleEmailWebhook(event: WebhookEvent) {
// Postmark doesnt sign webhooks. Protect the endpoint with
// HTTP Basic Auth and IP allowlisting (see the note below). As
// a safeguard, validate the payloads shape before acting on it
if (!event.RecordType || !event.MessageID) {
throw new Error("Unexpected webhook payload");
}
// Idempotency checks ensure events are processed once
const alreadyProcessed = await db.webhookEvents.findOne({
messageId: event.MessageID,
});
if (alreadyProcessed) {
return { status: "duplicate", messageId: event.MessageID };
}
await db.webhookEvents.create({ messageId: event.MessageID });
if (event.RecordType === "Delivery") {
return { status: "delivered", messageId: event.MessageID };
}
if (event.RecordType === "Bounce") {
return {
status: "bounced",
messageId: event.MessageID,
reason: event.Description,
};
}
// Acknowledge unhandled types too
return { status: "ignored", messageId: event.MessageID };
}
Route Handler
app.post(
"/webhooks/postmark",
express.json(),
async (req, res) => {
try {
const result = await handleEmailWebhook(req.body);
console.log("Webhook processed:", result.status, result.messageId);
res.sendStatus(200);
} catch (error) {
console.error("Webhook processing error:", error);
res.sendStatus(500);
}
}
);
Note: Postmark does not currently support HMAC webhook signature verification. The recommended approach to protect your webhook endpoint is HTTP Basic Authentication (described above) combined with allowlisting Postmark's IP ranges. We recommend validating the structure and content of incoming payloads in your handler as an additional safeguard.
A single Express route can handle both Delivery and Bounce event types from Postmark. Respond 200 once you've processed the event — or handed it to a durable queue. If processing fails, return a non-2xx status so Postmark retries. Check each MessageID against your database before acting so a retry doesn't process the same event twice.
For per-event typed handlers and full field references, see the delivery webhook → and bounce webhook → pages.
app.post("/webhooks/postmark", express.json(), async (req, res) => {
const event = req.body as { /* unchanged */ };
try {
// Idempotency: a retry of an event we've already handled is a no-op
const alreadyProcessed = await db.webhookEvents.findOne({
messageId: event.MessageID,
});
if (alreadyProcessed) return res.sendStatus(200);
await db.webhookEvents.create({ messageId: event.MessageID });
// ...Delivery and Bounce branches unchanged...
res.sendStatus(200);
} catch (error) {
console.error("Webhook processing error:", error);
res.sendStatus(500); // Postmark retries
}
});
Testing webhooks can be a difficult process. Here are two recommendations from Postmark.
A great tool for dealing with APIs (and many other things) is curl. In this context, you can use curl to make HTTP requests to your API in the same way that Postmark would make requests to your API. You can find an example curl call in each individual webhook section.
RequestBin is a great service to inspect HTTP requests. You can create a temporary RequestBin URL and use the temporary URL as your webhook URL in Postmark. RequestBin will then record the HTTP requests and allow you to inspect the HTTP requests to verify headers, JSON bodies, and other information about the request. This will provide you with information about the HTTP requests used in the webhook if you don’t have a public URL set up yet and want to start developing right away.
This covers outbound webhooks — the delivery, bounce, spam complaint, open, click, and subscription change events Postmark sends to your endpoint. Inbound webhooks are handled separately.
When Postmark delivers a webhook and the delivery fails, whether it retries depends on how your endpoint responded.
Retried (temporary failures):
Not retried — the event is dropped (permanent failures):
Redirects (3xx) are followed automatically (up to 10 hops); Postmark acts on the final response.
When a delivery is retried, Postmark uses an escalating back-off schedule — the same for every webhook type. A webhook that has been delivering normally is retried quickly at first; the longer it keeps failing, the further apart the attempts become. After the schedule is exhausted, Postmark stops retrying that individual event.
| Attempt | Delay after previous |
|---|---|
| 1 | 1 min |
| 2 | 5 min |
| 3 | 10 min |
| 4 | 10 min |
| 5 | 10 min |
| 6 | 15 min |
Every webhook request includes an X-PM-Retries-Remaining header so you can see how many attempts are left.
The retries above handle a single failed event. Separately, Postmark watches how each event type is doing across many events. If an event type fails every delivery attempt, we'll email you a warning and give you 48 hours to fix your endpoint. Postmark keeps sending events during that time. If nothing gets through, we disable that event type until you verify your webhook again. Your other event types aren't affected. See Webhook health notifications for what each email means and what to do.
Retries — and timeouts where your endpoint did the work but responded too slowly — mean your endpoint may occasionally receive the same event more than once. Make your handler idempotent so a repeated delivery doesn't cause duplicate side effects. Each payload includes a unique MessageID, and each delivery carries an X-PM-Webhook-Trace-Id header that stays stable across retries of the same event. Store one (or both) on receipt and check for it before acting, so a retried or timed-out-but-successful delivery is processed only once.
Postmark keeps an eye on the health of your webhooks. If your endpoint fails every attempt to deliver an event type, we'll email you so you can fix it before anything stops. This section explains each email and what to do next.
Health is tracked per event type, not per webhook. If your endpoint accepts Open events but fails on Click events, only Click is affected. Your other event types keep running as usual.
Note: In this section, "delivering an event" means Postmark sending that event to your endpoint and getting a 200 response. This is different from the Delivery event type, which tells you an email reached the recipient's server.
| What it means | Are events still sent? | What you need to do | |
|---|---|---|---|
| Warning | An event type is failing every delivery attempt | Yes | Fix your endpoint within 48 hours |
| Webhook healthy again | Your endpoint recovered within 48 hours | Yes | Nothing |
| Disabled webhook | An event type failed for the full 48 hours and is now disabled | No, for that event type only | Fix your endpoint, then verify your webhook again |
This webhook has been failing all attempts to deliver this event. You have 48 hours to fix it before we disable it.
Nothing has stopped yet. Postmark is still sending events of that type to your endpoint. If we do disable anything, it's only the event types listed in the email.
What to do:
Good news. An event of that type was delivered successfully within the 48 hours, so it's no longer at risk of being disabled.
What to do: Nothing. You don't need to verify your webhook again. Postmark kept sending events the whole time.
Your endpoint failed every attempt to deliver this event type for the full 48 hours, so we've disabled it. Postmark has stopped sending that event type to your endpoint. Other event types on the same webhook keep running as long as they're healthy.
What to do:
Events that happen while an event type is disabled aren't sent to your endpoint. They also aren't sent later, after you verify again. Only new events are sent.
Here are the most common causes:
Still stuck? Email support@postmarkapp.com and we'll help you get events flowing again.