Bounces, complaints & suppression

How bad addresses are handled and where to review them.

When an address hard bounces or complains, we add it to your suppression list. It's enforced on every send (single, batch, and campaign), including CC and BCC recipients.

  • Hard bounce — the address is invalid or rejected permanently; it's suppressed (reason: bounced).
  • Complaint — the recipient marked the mail as spam; it's suppressed (reason: complained).
  • Soft bounce — a temporary failure (full mailbox, greylisting); delivery is retried and the address isn't suppressed. Three soft bounces in a row suppress it (reason: bounced), and so does one to a domain that doesn't exist or accepts no mail.

Suppressed recipients are excluded, not failed — surfaced as a "Recipients excluded" event on the timeline. Review the list on the Suppressions tab. You can remove an address there to make it mailable again — but re-sending to bad addresses hurts deliverability.

Suppression is separate from marketing unsubscribe

The suppression list is distinct from a contact's marketing preference. An address can be unsubscribed from marketing but mailable for transactional sends, or suppressed outright. Manage blocks on the Suppressions page; manage preferences on the contact.

Before a big send

Before a campaign goes out, the Send dialog (and warnings[] in the validate response of the campaigns API) may show a few warnings. They never block the send; they point at the habits that most often get mail throttled or filtered. A scheduled campaign also warns when your scheduled sends may outrun the credits left this period, since a campaign that runs short pauses until you top up.

New domains and sudden volume

Mailbox providers judge a sending domain by its history. A domain that verified recently, or has never sent, has none, and a large first send looks like a bulk or compromised sender. Start with your most engaged contacts, send a few hundred emails on day one, and grow the volume over about a week. The same goes for a send far larger than your usual days: split it into a few sends over several days.

DMARC

Gmail and Yahoo require a DMARC record for bulk senders. Without one, more of your mail lands in spam or is refused. Add the DMARC record shown on your domain page; a policy of p=none is enough to start.

Sending frequency

Mailing the same people several times in a few days drives unsubscribes and spam complaints, which hurt every later send. Space marketing campaigns out, and leave out recipients who just heard from you.

Use your own unsubscribe on lifecycle email

Onboarding sequences, digests and alerts you render yourself and send as html/text are not marketing-category sends: we add no footer and no unsubscribe header to them. If you run your own unsubscribe page and list, put your headers on the send and Gmail shows its native Unsubscribe button (it is built from the List-Unsubscribe header, not from a link in the body — which is also what Google Postmaster Tools checks for one-click). It works on every plan, right away: no setting to turn on and nothing to request. The same headers work on a single send, each batch item, SMTP, and the MCP email.send tool.

  • List-Unsubscribe — one or more <…> entries separated by commas, each an https:// URL or a mailto: address. A mailto-only header is fine. Plain http://, other schemes and non-ASCII characters are refused with a 422: percent-encode non-ASCII characters in the URL and write an international domain in its punycode (xn--…) form. Keep the header to 870 characters or fewer.
  • List-Unsubscribe-Post — exactly List-Unsubscribe=One-Click, for one-click. It needs at least one https:// entry in List-Unsubscribe (the inbox POSTs to it); mailto alone can't take a one-click.
  • Set each header once. Header names are case-insensitive, so List-Unsubscribe and list-unsubscribe on the same send is refused.
  • Keep an unsubscribe link in the body too: inboxes that don't render the header still need one. Our content review reads the header the way it reads the body; it never holds or slows a send because of the header.
One-click (https + mailto)
curl https://www.unitpost.com/api/v1/email \
  -H "Authorization: Bearer $UNITPOST_API_KEY" \
  -H "Content-Type: application/json" \
  -H "User-Agent: my-app/1.0" \
  -d '{
    "from": "Jobs <jobs@mail.acme.com>",
    "to": "ada@example.com",
    "subject": "Your weekly digest",
    "html": "<p>New jobs for you. <a href=\"https://acme.com/u/abc\">Unsubscribe</a></p>",
    "headers": {
      "List-Unsubscribe": "<https://acme.com/u/abc>, <mailto:unsub@acme.com>",
      "List-Unsubscribe-Post": "List-Unsubscribe=One-Click"
    }
  }'
Over SMTP (Nodemailer)
await transporter.sendMail({
  from: "Jobs <jobs@mail.acme.com>",
  to: "ada@example.com",
  subject: "Your weekly digest",
  html: digestHtml,
  headers: {
    "List-Unsubscribe": "<https://acme.com/u/abc>, <mailto:unsub@acme.com>",
    "List-Unsubscribe-Post": "List-Unsubscribe=One-Click",
  },
});

Over SMTP the two headers are read from the message itself. Mail libraries fold long header lines on their own; that's fine, we unfold them before checking.

Mailto only
curl https://www.unitpost.com/api/v1/email \
  -H "Authorization: Bearer $UNITPOST_API_KEY" \
  -H "Content-Type: application/json" \
  -H "User-Agent: my-app/1.0" \
  -d '{
    "from": "Jobs <jobs@mail.acme.com>",
    "to": "ada@example.com",
    "subject": "Your weekly digest",
    "text": "Jobs for you this week. Reply to this message to unsubscribe.",
    "headers": { "List-Unsubscribe": "<mailto:unsub@acme.com?subject=unsubscribe>" }
  }'

Marketing templates keep our unsubscribe

A send from a marketing-category template refuses these two headers (422): Unitpost adds its own per-recipient List-Unsubscribe and footer there, and records the opt-out on the contact. Your own headers are for mail whose unsubscribe you handle yourself — those opt-outs land on your endpoint and your list, not on the contact. Opt-outs recorded with us still don't block transactional sends.

When a campaign pauses itself

We watch every email campaign while it sends. If too many of its addresses don't exist, or too many recipients mark it as spam, we pause that campaign, not your account: your password resets, receipts and other campaigns keep sending. The campaign page shows what happened in plain numbers, and the campaign.paused webhook carries the same facts.

SignalJudged once there areWarning atPaused at
Addresses that don't exist (hard bounces), per message sent5 hard bounces—10%
Addresses that don't exist (hard bounces), per message sent10 hard bounces2%5%
Spam complaints, per message delivered3 complaints0.05%0.1%

Small numbers get a margin: we act only when we're 95% sure the real rate is at or above the line. So 3 bounces out of 10 sends doesn't pause yet, but 5 out of 20 does, and so do 98 out of 1,400. Temporary failures (a full mailbox, greylisting) never count. While the platform's overall bounce rate is high, the 5% line tightens to 3%. Our delivery reviewer, Unit, can also pause a campaign earlier when the pattern is clearly bad (for example, most of the first sends bouncing). It can only add caution; it never keeps a campaign sending past these lines. We may also tighten these lines for a while, for example during an incident; the campaign page and the webhook always show the line that applied.

  • The first time, you can resume it yourself: download or remove the affected addresses (they're already suppressed), confirm the list was opted in and cleaned, and resume. Only sends made after you resume count toward the lines, so old bounces won't pause it again.
  • A repeat needs a quick review by our team: this campaign was paused automatically before (at any time), or another of your campaigns was in the last 90 days. Resume answers 409 review_required and the page offers Request review. A third goes to an account review.
  • Cancel the rest stops the campaign for good; nothing more is sent.

In the API, a paused campaign carries pause_reason: a code (hard_bounce_rate, complaint_rate, quality_review, or a non-quality reason such as user, billing or failure_streak), the plain message, and for an automatic quality pause the metric, value, threshold, sample, window, decided_by and resume (self_serve, review_required or account_review).

Get these as webhooks

Every lifecycle event is available in-app today, and you can also have us POST them to your own endpoint in real time. See the Webhooks guide to register an endpoint and verify the signed payloads.

Search docs and guides

Search the docs and product guides.