When Your AI Agent's Email Bounces: Hard vs Soft Bounces and What to Automate
Hard bounce, soft bounce or complaint? How an AI agent should react to each, why it must never guess addresses, and how to wire bounce events into your agent with webhooks.
By Toan Nhu

An agent that sends email will eventually get a bounce. The question is what happens next. A human glances at the failure notice and moves on. An agent, left to its own devices, might retry the same dead address every hour, guess a new address from a name and a domain, or treat a temporary delay as a permanent failure and drop an important conversation. Bounce handling is one of the least glamorous parts of agent email, and one of the most important for keeping your sending reputation healthy.
Three outcomes that are easy to confuse
- Hard bounce. The receiving server says the message can never be delivered: the mailbox does not exist, the domain has no mail server, or the address is malformed. SMTP codes in the 5xx range usually signal this. Retrying will not help.
- Soft bounce. Delivery failed for now: a full mailbox, a server that is temporarily unavailable, a message that is too large, or rate limiting on the receiving side. These typically come back as 4xx codes. The address may work later.
- Complaint. The message was delivered, and the recipient marked it as spam. This is not a bounce at all, but it is the strongest negative signal you can get, and it should stop further mail to that person immediately.
Codes are hints, not gospel. Providers are inconsistent, and some report policy blocks (for example, a message rejected for reputation reasons) with a permanent code even though the address is valid. Treat the classification your email platform gives you as the starting point, and log the raw reason for later review.
Why agents make bounce problems worse
Mailbox providers watch how senders behave after a failure. Repeatedly mailing addresses that do not exist looks like someone working from a stale or purchased list, and that erodes your domain's reputation for every future message. Agents amplify this in a few specific ways:
- They retry by default. A tool call that fails is often retried by the agent loop, and a send that bounces later may look like it never happened.
- They improvise addresses. Asked to reach a person, a model may try firstname@company.com, then first.last@company.com. Each guess that fails is a hard bounce on your record, and a guess that succeeds may reach the wrong person.
- They confuse accepted with delivered. An API that returns success usually means the message was accepted for sending, not that it arrived.
The fix is to make bounce behavior a deterministic rule in your code, not a judgment call in the prompt.
A simple policy your agent can follow
- Hard bounce: mark the address as undeliverable, stop every queued or scheduled message to it, and tell the human owner of the task. Do not try variants.
- Soft bounce: allow a small, fixed number of later attempts with growing gaps between them. If the address keeps soft bouncing over several days, treat it like a hard bounce.
- Complaint: suppress the address for all non-essential mail and flag the conversation for review. Ask why the message felt unwanted.
- Uncertain result: look for the existing message before sending again, so a flaky network call does not produce a duplicate.
Prevent bounces before they happen
The cheapest bounce is the one you never send. For agents, that mostly means controlling where addresses come from. Only let an agent email addresses that came from a trusted source: a reply to a thread, a CRM record, a signup form with confirmation, or a list a human approved. If an address is missing, the agent should report that it is missing rather than infer one.
For larger imported lists, a verification service can catch syntax errors, dead domains and many non-existent mailboxes before you send. Be aware of catch-all domains, which accept mail for any address at the SMTP stage and bounce or discard it later, so a verification check cannot confirm the individual mailbox. Treat catch-all results as unknown, send to them in small numbers, and watch the outcomes.
Wiring bounce events into your agent with Mermail
Mermail separates the stages a message goes through. When your agent sends, the returned status tells you whether it was drafted, scheduled, queued or sent, and Mermail's own docs are explicit that sent means the message was accepted, not that it reached the recipient. Delivery outcomes arrive separately as webhook events:
- message.sent: the Email module accepted the message for sending.
- message.delivered: the recipient's server confirmed delivery.
- message.bounced: a bounce was reported.
- message.complained: a recipient reported the message as spam.
Delivery outcome payloads can include the affected recipient, the outcome and a bounce_type, which matters when one message went to several people and only one address failed. Every delivery is signed, and each event carries a stable event_id you should use to deduplicate retries. A minimal handler looks like this:
// Runs after you have verified the webhook signature
// and deduplicated on event.event_id.
type DeliveryEvent = {
event_id: string;
event_type: string;
data: { recipient?: string; outcome?: string; bounce_type?: string };
};
export async function onDeliveryEvent(event: DeliveryEvent, store: SuppressionStore) {
const address = event.data.recipient?.toLowerCase();
if (!address) return; // log and review: no recipient to act on
switch (event.event_type) {
case 'message.bounced':
// Map bounce_type values to your own policy after checking real payloads.
await store.recordBounce(address, event.data.bounce_type ?? 'unknown');
break;
case 'message.complained':
await store.suppress(address, 'complaint');
break;
case 'message.delivered':
await store.recordDelivered(address);
break;
}
}
interface SuppressionStore {
recordBounce(address: string, kind: string): Promise<void>;
suppress(address: string, reason: string): Promise<void>;
recordDelivered(address: string): Promise<void>;
}Before the agent's send tool runs, check the address against that store and refuse the call if it is suppressed. Enforcing it in the tool layer means a clever prompt cannot talk the agent past it. Mermail plans also include suppression lists on the platform side, and signed webhooks are available on the Developer plan and above.
What to measure
Track bounces and complaints per mailbox and per task, not just in aggregate. A single workflow pulling addresses from a bad source can hide inside a healthy overall number. There is no universal safe bounce rate, but a sudden rise is always worth pausing for. Google, for example, asks bulk senders to keep reported spam rates below 0.3 percent, and a well-run agent sending to people who expect its mail should sit far below that.
If numbers climb, stop sending from the affected workflow, find the source of the bad addresses, and resume slowly. Pausing for a day is far cheaper than repairing a damaged domain.
Give your agent an inbox that reports what really happened to its mail. Start free with Mermail.


