DEV Community

Qasim Muhammad
Qasim Muhammad

Posted on • Originally published at developer.nylas.com

Handle Bounced Email in Agent Outreach

A failed send throws an error your code can catch; a bounce happens minutes later, in someone else's mail server, after your API call already returned success. That gap is where outreach agents quietly rot. The agent fires off a campaign, every send returns 200, the dashboard's green — and a chunk of those messages died at addresses that no longer exist. Without bounce handling you never learn which ones, so the agent keeps emailing dead mailboxes, and every retry chips away at the sender reputation your deliverable mail depends on.

The fix is event-driven: bounces arrive as webhooks, and your agent's job is to listen and adapt.

Where bounce events come from

When a recipient's server rejects a message, the provider generates a Non-Delivery Report — that "Mail Delivery Subsystem" email humans glance at and archive. Nylas watches for NDRs in the sender's mailbox and converts them into a structured message.bounce_detected webhook, with the failed address, the reason, and the SMTP code already parsed out.

Subscribe to it like any other trigger by adding message.bounce_detected to your webhook's trigger_types:

curl --request POST \
  --url "https://api.us.nylas.com/v3/webhooks" \
  --header "Authorization: Bearer <NYLAS_API_KEY>" \
  --header "Content-Type: application/json" \
  --data '{
    "trigger_types": ["message.bounce_detected", "message.send_failed"],
    "callback_url": "https://yourapp.example.com/webhooks/nylas"
  }'
Enter fullscreen mode Exit fullscreen mode

For connected mailboxes, detection works on 4 providers — Google, Microsoft, iCloud, and Yahoo — because it depends on the provider issuing an NDR; generic IMAP and Exchange (EWS) accounts don't produce these events. If your outreach runs from a Nylas Agent Account (the hosted agent mailboxes, currently in beta), the platform owns the SMTP path end-to-end, so message.send_success, message.send_failed, and message.bounce_detected give you send-side visibility on every outbound message.

Reading the payload

Five fields carry the signal, per the bounce handling recipe:

{
  "type": "message.bounce_detected",
  "data": {
    "grant_id": "<NYLAS_GRANT_ID>",
    "object": {
      "bounced_addresses": "no-such-user@example.com",
      "bounce_reason": "The email account that you tried to reach does not exist.",
      "type": "mailbox_unavailable",
      "code": "550",
      "bounce_date": "Mon, 08 Jun 2026 14:21:00 +0000"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

bounced_addresses is the address that failed, bounce_reason is the human-readable explanation, type is a category like mailbox_unavailable, and code is the SMTP status — note it's a string, so compare against "550", not 550. The payload also includes origin, the original message, which is how you tie the bounce back to the campaign and contact record that triggered it.

Hard vs. soft: the only branch that matters

The code field splits bounces into two categories with opposite responses:

  • 5xx — hard bounce. The address is invalid or the mailbox is gone. Permanent. Add the address to a suppression list and never send to it again. Retrying a hard bounce isn't just wasted effort — it actively erodes your sending reputation, which lowers inbox placement for everything else you send.
  • 4xx — soft bounce. A full mailbox, a throttled server, some temporary condition. Safe to retry — but with a cap. Two or three attempts over a couple of days; if it's still failing, treat it as hard.

For an outreach agent specifically, the suppression list should sit upstream of the send, as a pre-send check in the agent's pipeline. The flow becomes: agent selects contacts → filters against suppressions → sends → bounce webhooks feed new suppressions back in. The loop closes itself, and the list only grows more accurate.

Agent-specific behaviors worth wiring in

Beyond per-address suppression, a bounce stream enables campaign-level reflexes that a send-and-forget script can't have:

Back off on bounce spikes. If a batch of sends produces a cluster of hard bounces, that's a data-quality signal — a stale list, a bad enrichment source. An agent that pauses the campaign and flags the list for review protects the sending domain; one that plows through the remaining contacts compounds the damage. This matters doubly on agent infrastructure, where sender reputation is shared across every account on the domain.

Annotate, don't just suppress. Write the bounce_reason and type back to your CRM or contact store. "Suppressed: mailbox_unavailable, 550, June 2026" tells a future human (or agent) why this contact went dark.

Expect latency. Detection is asynchronous — the NDR can land minutes after the send. Don't design a flow that assumes bounce status is known immediately after the API call returns; reconcile on the webhook, not inline.

One scoping note: this covers mailbox sends. If you're sending through the transactional Email API instead, the equivalent signal is message.transactional.bounced — one of 4 transactional deliverability events alongside complaint, delivered, and rejected.

What the platform does when you don't

If your outreach runs on an Agent Account, there's an enforcement layer behind your suppression list. Nylas tracks each account's rolling hard-bounce rate — soft bounces don't count — against its recent send volume, per the send limits docs:

Bounce rate Account state What happens
Under 2% Healthy Normal sending.
5% or above Under review Sending continues; sustained elevated bounces lead to a pause.
10% or above Sending paused Outbound send requests fail until Nylas clears the pause.

Two details make this worth designing around rather than discovering. "Under review" is silent — your code sees nothing until the pause hits, at which point sends start returning 400 errors. And pauses don't clear on a timer: resuming requires contacting support with the cause and the fix. An agent that suppresses hard bounces aggressively stays comfortably under 2% and never meets this table; an agent that ignores them works its way down it. Complaint rates get the same treatment with much tighter numbers — review at 0.1%, paused at 0.5% — so honor unsubscribes immediately too.

Start with the table

The minimal viable version is genuinely small: one webhook subscription, one suppressions table with address, code, reason, and date columns, and one WHERE NOT IN clause on your send query. You can add retry budgets and spike detection later; the suppression check alone stops the reputation bleed.

Pull your outreach agent's sends from the last month and check how many went to addresses that had already hard-bounced. If the answer isn't zero, that's your sprint.

Top comments (0)