Overview

Integration

User guide

API reference

Webhooks

Webhooks overview

What is a webhook? #

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.

Verifying your webhook #

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.

Protecting your webhook #

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.

IPs for firewalls

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.

Basic HTTP authentication

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.

Handle delivery and bounce events #

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.

Combined delivery and bounce handler #

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 your webhook #

Testing webhooks can be a difficult process. Here are two recommendations from Postmark.

Using curl

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.

Using RequestBin

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.

Retry attempts #

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

  • 5xx server errors (500–599)
  • 408 Request Timeout
  • 429 Too Many Requests
  • Network-level failures — the request times out or the connection can't be established

Not retried — the event is dropped (permanent failures):

  • Any other 4xx client error — for example 400, 401, 403, 404, 405, 410, 422. A 4xx means the request as sent won't succeed on a retry, so Postmark doesn't retry it.

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.

When an endpoint keeps failing

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.

Avoid duplicate processing 

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.

Webhook health notifications #

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.

How webhook health works

  1. Your endpoint fails every attempt to deliver an event type. Each failed event is still handled by the usual retry rules.
  2. We send you a warning email. You have 48 hours to fix your endpoint. Postmark keeps sending events the whole time.
  3. Then one of two things happens:
    • An event of that type is delivered successfully within 48 hours. We send a "webhook healthy again" email. You don't need to do anything else.
    • No event of that type is delivered successfully within 48 hours. We disable that event type and send a disabled webhook email. Postmark stops sending that event type until you fix your endpoint and verify your webhook again.
EmailWhat it meansAre events still sent?What you need to do
WarningAn event type is failing every delivery attemptYesFix your endpoint within 48 hours
Webhook healthy againYour endpoint recovered within 48 hoursYesNothing
Disabled webhookAn event type failed for the full 48 hours and is now disabledNo, for that event type onlyFix your endpoint, then verify your webhook again

I received a warning email

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:

  1. Check the endpoint URL and event types listed in the email.
  2. See what your endpoint is returning. You can check recent successes and failures for each event type in the webhook settings in Postmark or with Get webhook statistics.
  3. Fix the problem. See Why is my endpoint failing? below for common causes.
  4. That's it. As soon as your endpoint accepts an event of that type, the warning clears on its own and we'll send you a "webhook healthy again" email.

I received a "webhook healthy again" email

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.

I received a disabled webhook email

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:

  1. Fix your endpoint. See Why is my endpoint failing? below for common causes. You can check per-event verification status and recent results with Get webhook statistics.
  2. Verify your webhook again. We can't start sending that event type again until you do.
    • In the app: Open the webhook and select Verify & Save.
    • With the API: Call POST /webhooks/{ID}/verify.
  3. Watch for new events. Once your webhook passes verification, Postmark starts sending that event type again.

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.

Why is my endpoint failing?

Here are the most common causes:

  • Your endpoint returns an error. Any response other than a 200 counts as a failure. Check your application logs for errors around the times shown in your webhook statistics.
  • The URL changed or was removed. A 404 or 410 usually means the route moved or was deleted during a deploy.
  • Authentication changed. If you use basic HTTP authentication, make sure the username and password in your webhook URL still match your server.
  • Your firewall is blocking Postmark. Make sure you allow Postmark's webhook IP ranges.
  • Your endpoint is too slow. If your endpoint doesn't respond in time, the request counts as a failure. Respond with a 200 first, then process the event or hand it off to a queue.
  • Your SSL certificate expired or isn't valid. Renew or replace it, then verify your webhook again.

Still stuck? Email support@postmarkapp.com and we'll help you get events flowing again.