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.
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.
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.
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.
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.
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.
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.
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.
{
"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>"
}
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.
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.
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 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>;
};
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,
};
}
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);
}
}
);