Overview

Integration

User guide

API reference

Webhooks

Bounce webhook

What is a bounce webhook? #

One of the reasons you send emails through Postmark is to handle all of the possible bounces that email servers can return. But, your application still needs the information about those bounces in an easy-to-use format that you can process. You could use the Bounce API to pull data about them but using webhooks allows Postmark to push these events to you as they happen.

A bounce webhook will push a JSON event to your application right after Postmark processes the bounce report. The events are triggered when inbox providers respond to email sending through Postmark with outcomes such as a hard bounce, soft bounce, undeliverable, etc.

What can I do with these events?

  • Help users correct their email if their sign up email address bounces.
  • Suppress bounced emails from sending which can hurt your sending reputation.
  • Reactivate email addresses due to hard bounces using the activate bounces API.
  • Generate statistics that are specific to your application.
  • Alert your team when certain emails bounce.

Official Postmark Slack App

Side note: an easy way to get bounce notifications is through our official Slack App. Search for "Postmark Bot" in the Slack App Directory, or install directly from the Slack app page.

Use Rebound to prompt your users to update their email address

If you're not ready to build full bounce handling into your application, Rebound is a Javascript snippet you install on your website. It checks the Postmark API for hard bounces and prompts customers to update their email address if an email you sent them hard bounced.

Use Zapier to notify your senders with an email if a message they sent bounced

For a low effort method of using bounce webhooks to alert your senders of bounces, check out our help article on using Zapier to automatically alert your senders of bounces.

Create a new webhook #

In your Postmark account — select a Server, Message Stream, and navigate to the Webhooks settings tab. Add a new webhook with your URL and toggle the Bounce type. You can also use the API.

Using the API

Set Triggers.Bounce.Enabled to true to enable this event type when you create or edit webhooks. Whichever method you choose, verify everything works as expected before real events start flowing.

Event data #

An example of the full JSON document that would be POSTed to your webhook URL. A brief description of some of the critical fields are listed below. Spam complaints, subscription changes, and manual deactivations have their own webhooks that are not triggered with this event type.

  • ID — Identifier for making Bounce API requests.
  • TypeCode — Identifies the type as a defined integer (e.g. hard bounce, soft bounce, transient).
  • Type - Pairs with the TypeCode classification as a defined string.
  • Metadata — Custom data that was included in the email.
  • Email — The email address that bounced.
  • From — The original sender of the bounced email.
  • BouncedAt — Timestamp of when bounce occurred (ISO 8601 format)
  • Inactive — Lets you know if this bounce caused the email address to be deactivated.
  • CanActivate — Lets you know if this address can be activated again.
  • Content — The full content of the email bounce. This is off by default but can be enabled.

Important TypeCodes

TypeCode 1 — hard bounce: the email address is invalid and should be suppressed from receiving further emails. Sending to hard-bounced addresses will hurt your sending reputation!

TypeCode 4096 — soft bounce: check Inactive to see whether Postmark deactivated the address, and CanActivate for whether you can reactivate it.

Every other type, including Transient (TypeCode 2) for a temporary delivery failure, is listed on the Bounce API reference.

Example JSON webhook data

{
  "RecordType": "Bounce",
  "MessageStream": "outbound",
  "ID": 692560173,
  "Type": "HardBounce",
  "TypeCode": 1,
  "Name": "Hard bounce",
  "Tag": "Test",
  "MessageID": "883953f4-6105-42a2-a16a-77a8eac79483",
  "Metadata": {
    "PropA": "some value",
    "PropB": "some value"
  },
  "ServerID": 23,
  "Description": "The server was unable to deliver your message (ex: unknown user, mailbox not found).",
  "Details": "Test bounce details",
  "Email": "margareth@nasa.com",
  "From": "alanturing@computers.com",
  "BouncedAt": "2026-11-05T16:33:54.9070259Z",
  "DumpAvailable": true,
  "Inactive": true,
  "CanActivate": true,
  "Subject": "Saying Hello!",
  "Content": "<Full dump of bounce>"
}

Testing events #

If you’re developing on your local machine or don’t have a public URL for your API, the cURL request example below sends a test webhook to your service. Replace <your-webhook-url>, run the command, and verify it accepts and processes the event as expected.

Example cURL

curl <your-webhook-url> \
  -X POST \
  -H "Content-Type: application/json" \
  -d '{
  "RecordType": "Bounce",
  "MessageStream": "outbound",
  "ID": 692560173,
  "Type": "HardBounce",
  "TypeCode": 1,
  "Name": "Hard bounce",
  "Tag": "Test",
  "MessageID": "883953f4-6105-42a2-a16a-77a8eac79483",
  "Metadata": {
    "PropA": "some value",
    "PropB": "some value"
  },
  "ServerID": 23,
  "Description": "The server was unable to deliver your message (ex: unknown user, mailbox not found).",
  "Details": "Test bounce details",
  "Email": "margareth@nasa.com",
  "From": "alanturing@computers.com",
  "BouncedAt": "2026-11-05T16:33:54.9070259Z",
  "DumpAvailable": true,
  "Inactive": true,
  "CanActivate": true,
  "Subject": "Saying Hello!",
  "Content": "<Full dump of bounce>"
}'

You can also generate fake bounces by using our blackhole domain. This allows you to test your bounce webhook in a safe way so the bounces don't affect your sending reputation. Learn more about how to test bounces.

Code examples #

If you're new to setting up webhook event handlers, you can find a Express/Node/Typescript code example below to help you get started. This handler parses the bounce payload and returns a normalized result that distinguishes hard bounces (permanent; suppress future sends) from soft bounces (temporary; may recover). Protect the endpoint with HTTP Basic Authentication and IP allowlisting rather than a signature — Postmark doesn't sign webhooks.

Store an identifier for each event on receipt and check for it before processing. Postmark retries on any non-2xx response or when your endpoint times out. As a result, the same bounce event may arrive more than once. The X-PM-Webhook-Trace-Id header, when available, uniquely identifies the delivery and is the better key.

If it is not available, you can create a compound key using multiple fields including unique identifiers like MessageID, Email, and BouncedAt.

Type Declaration

type BounceEvent = {
  RecordType: string;
  MessageID: string;
  Email: string;
  Type: string;
  TypeCode: number;
  Description: string;
  BouncedAt: string;
  Inactive: boolean;
  CanActivate: boolean;
  Tag?: string;
  Content?: string;
  Metadata: Record<string, string>;
};

Event Handler

export async function handleBounceWebhook(event: BounceEvent) {
  // Ensure non-bounce events are not processed
  if (event.RecordType !== "Bounce") {
    return { status: "skipped", messageId: event.MessageID };
  }

  // Idempotency checks ensure events are processed once
  const alreadyProcessed = await db.bounceEvents.findOne({
    messageId: event.MessageID,
  });
  if (alreadyProcessed) {
    return { status: "duplicate", messageId: event.MessageID };
  }

  // TypeCode 1 is a hard bounce because the Email address is invalid and should be suppressed
  const isHardBounce = event.TypeCode === 1;

  await db.bounceEvents.create({
    messageId: event.MessageID,
    email: event.Email,
    bounceType: event.Type,
    typeCode: event.TypeCode,
    reason: event.Description,
    bouncedAt: event.BouncedAt,
    inactive: event.Inactive,
    canActivate: event.CanActivate,
  });

  // Continuing to send to hard-bounced email addresses damages your sending reputation
  if (isHardBounce) {
    await db.suppressedAddresses.upsert({ email: event.Email });
  }

  return {
    status: "processed",
    messageId: event.MessageID,
    hardBounce: isHardBounce,
    recoverable: event.CanActivate,
    reason: event.Description,
  };
}

Application Routing

app.post(
  "/webhooks/postmark/bounce",
  express.json(),
  async (req, res) => {
    try {
      const result = await handleBounceWebhook(req.body);
      console.log("Bounce webhook processed:", result.status, result.messageId);
      res.sendStatus(200);
    } catch (error) {
      console.error("Bounce webhook error:", error);
      res.sendStatus(500);
    }
  }
);