How to Handle Email Bounces via Webhook

Automate bounce handling by routing bounce notification emails to your webhook. JsonHook parses bounce messages and delivers structured JSON so your app can update contact lists in real time.

Table of Contents
  1. Overview
  2. Prerequisites
  3. Step-by-Step Instructions
  4. Code Example
  5. Common Pitfalls

Overview

Email bounces occur when a message cannot be delivered to the recipient. Hard bounces (permanent failures — invalid address, domain does not exist) must be removed from your sending list immediately to protect your sender reputation. Soft bounces (temporary failures — mailbox full, server unavailable) should be retried but tracked.

Most email service providers send bounce notification emails to a designated bounce address. By routing that bounce address through JsonHook, your application can process bounce events programmatically and update your contact database in real time — no manual list cleaning, no cron jobs polling an IMAP mailbox.

The typical flow is:

  • Your ESP sends a bounce notification to [email protected]
  • You forward that address to a JsonHook inbound address
  • JsonHook delivers the parsed bounce email as JSON to your webhook
  • Your handler extracts the bounced address and updates your database

Prerequisites

To automate bounce handling you need:

  • A JsonHook inbound address pointed at a bounce-handling webhook endpoint
  • Your ESP configured to send bounce notifications to that address (or a forwarding rule in your DNS/email provider)
  • A database or contact list that stores email addresses and their deliverability status
  • Understanding of the bounce email format your ESP uses — examine a few real bounce emails to identify where the bounced address appears

Automate Your Bounce Processing

Route bounce notifications through JsonHook. Keep your email list clean automatically.

Get Free API Key

Step-by-Step Instructions

Set up automated bounce handling:

  1. Create a JsonHook address for bounces:
    POST /v1/addresses
    { "webhookUrl": "https://yourapp.com/webhooks/bounces", "label": "Bounce handler" }
  2. Configure your ESP to send bounce notifications to your JsonHook address. In SendGrid, set the bounce notification address in Settings → Mail Settings → Bounce Purge. In Mailgun, set the bounce webhook or forward the default bounce address.
  3. Examine a test bounce payload. Send an email to a known invalid address and inspect the resulting bounce notification in your webhook payload. The bounced address is typically in the email body — look for patterns like "Original-Recipient:", "Final-Recipient:", or "failed permanently" followed by the address.
  4. Write extraction logic for the bounced address from email.textBody.
  5. Update your database — mark hard-bounced addresses as undeliverable and remove them from active sending lists.
  6. Differentiate hard vs soft bounces — look for "permanent" or "5xx" in the bounce message for hard bounces; "temporary" or "4xx" for soft bounces.

Code Example

A Node.js handler that parses bounce notifications and updates a contact database:

import express from "express";
import crypto from "crypto";
import { db } from "./db";

const app = express();
app.use(express.raw({ type: "application/json" }));

function extractBounceInfo(textBody: string) {
  // RFC 3464 DSN format — most ESPs use this
  const finalRecipient = textBody.match(
    /Final-Recipient:s*rfc822;s*([^
]+)/i
  )?.[1]?.trim();
  const status = textBody.match(/Status:s*([d.]+)/i)?.[1];
  const isHard = status?.startsWith("5") ?? false;
  return { bouncedAddress: finalRecipient, isHard, status };
}

app.post("/webhooks/bounces", async (req, res) => {
  const sig = req.headers["x-jsonhook-signature"] as string;
  const expected = crypto
    .createHmac("sha256", process.env.JSONHOOK_SECRET!)
    .update(req.body).digest("hex");
  if (sig !== expected) return res.sendStatus(401);

  const { email } = JSON.parse(req.body.toString());
  const { bouncedAddress, isHard, status } = extractBounceInfo(
    email.textBody ?? ""
  );

  if (bouncedAddress) {
    await db.contacts.update(
      { email: bouncedAddress },
      { bounceStatus: isHard ? "hard" : "soft", bounceCode: status }
    );
    if (isHard) {
      await db.contacts.update({ email: bouncedAddress }, { active: false });
    }
    console.log(`Bounce: ${bouncedAddress} (${isHard ? "hard" : "soft"}, ${status})`);
  }

  res.sendStatus(200);
});
app.listen(3000);

Common Pitfalls

Avoid these bounce-handling mistakes:

  • Unsubscribing on soft bounces. Soft bounces (4xx status codes) are temporary — do not remove the address from your list on the first soft bounce. Apply a retry threshold (e.g., 3 consecutive soft bounces) before deactivating.
  • Not handling DSN format variations. Different ESPs generate slightly different DSN (Delivery Status Notification) formats. Test with actual bounce emails from your specific ESP and write extraction logic accordingly.
  • Missing the nested MIME structure. Bounce emails are often multipart with a machine-readable part (message/delivery-status) and a human-readable explanation. The machine-readable part is the most reliable extraction target.
  • Acting on out-of-order bounces. A bounce notification may arrive after a contact has been re-verified. Check the bounce timestamp against your database before marking as inactive.
  • Not logging unrecognized bounce formats. When extraction returns null, log the full textBody for manual review rather than silently discarding the bounce.

Frequently Asked Questions

Can I receive bounce webhooks from any ESP?

Yes. Any ESP that can forward or deliver bounce notification emails to a custom address can feed into JsonHook. You configure your bounce address to forward to your JsonHook inbound address, and JsonHook delivers the parsed notification to your webhook regardless of which ESP generated the original bounce.

What is the RFC 3464 DSN format?

RFC 3464 defines the standard format for Delivery Status Notifications — the machine-readable bounce emails that mail servers generate. They contain headers like Final-Recipient, Status, and Action that describe which address bounced and why. Most modern ESPs generate RFC 3464 compliant bounces, making regex-based extraction straightforward.

How do I distinguish between a bounce and an out-of-office reply?

Bounces typically come from a mailer-daemon or postmaster address and have a specific DSN format. Out-of-office replies come from the recipient's actual address. Check email.from for MAILER-DAEMON, postmaster, or Mail Delivery Subsystem, and check for the message/delivery-status MIME part indicator in the email body.

Should I process bounce notifications synchronously or asynchronously?

Asynchronously. Push the bounce data to a queue and return 200 immediately. Database updates and list management operations should happen in background workers so your webhook handler stays fast and reliable.