Pre-Black Friday: 20% off, for as long as you stayEARLYBFon Solo and AgencyEnds Nov 28Start free

How-to guide

How to Manage Email Bounces (From Bounce Checker to Suppression List)

A bounce checker tells you an address failed. Bounce management stops the next one. Five steps with real queries, a suppression script and the counts behind the gate.

Get your free API key →Free to start. No credit card. 1,000 records to spend whenever you like.

A bounce checker is a one-time scan. Bounce management is a loop: classify each failure, suppress the dead addresses, gate the next list on email status, and watch the rate. This guide builds that loop in five steps.

Other guides on this topic stay conceptual. This one shows the SMTP codes, the script and the API queries. Examples use placeholders (Acme, acme.com, Jane Doe).

TL;DR

  • Classify every bounce by its enhanced status code, not by the word in the message. 5.x.x is permanent, 4.x.x is temporary.
  • Suppress permanent failures at once. Gate the next list on emailStatus before you pull it. Sizing is free.
  • Takes about 25 minutes and spends about 3 of the 1,000 free LeadOcean records.

Prerequisites

  • A LeadOcean key from app.leadocean.io, exported as $LEADOCEAN_API_KEY. The Free plan works: no card, 1,000 records for life.
  • A bounce log from your sender: a CSV with email and the raw bounce message.
  • curl, jq and Python 3.

All requests go to https://api.leadocean.io with the header x-api-key.

Step 1: Classify each bounce by its code

The enhanced status code in the bounce message says whether a retry can ever work. The format is defined in RFC 3463 (checked September 2026): a first digit of 4 is a persistent transient failure, 5 is a permanent failure.

python
import csv, re, sys

CODE = re.compile(r"\b([245])\.(\d{1,3})\.(\d{1,3})\b")

def classify(message: str) -> str:
    m = CODE.search(message)
    if not m:
        return "unknown"
    cls, subject, detail = m.group(1), m.group(2), m.group(3)
    if cls == "5":
        return "hard"          # 5.1.1 is a mailbox that does not exist
    if cls == "4":
        return "soft"          # 4.2.2 is a full mailbox
    return "ok"

for row in csv.DictReader(open(sys.argv[1])):
    print(row["email"], classify(row["message"]))

Treat unknown as soft for one retry and hard after that. Do not trust keywords like "blocked" in free text: a 5.7.1 policy block is permanent for that message, but it says nothing about the mailbox.

Step 2: Build the suppression list

Hard bounces go onto a suppression list the same day. Soft bounces get a retry count, and the third failure moves to the list.

python
from collections import Counter

soft = Counter()
suppress = set()

for email, kind in classified:        # output of Step 1
    if kind == "hard":
        suppress.add(email.lower())
    elif kind in ("soft", "unknown"):
        soft[email.lower()] += 1
        if soft[email.lower()] >= 3:
            suppress.add(email.lower())

open("suppress.txt", "w").write("\n".join(sorted(suppress)))

Load suppress.txt into your sequencer's global block list, not into one campaign. A dead address that is only blocked in one campaign bounces again in the next.

Step 3: Gate the next list on email status

The cheapest bounce is the one you never send. Size the list by status before you pull it. A count returns a total and no rows, and costs nothing.

bash
curl -s -X POST "https://api.leadocean.io/v1/people/search?count=true&limit=1" \
  -H "x-api-key: $LEADOCEAN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"jobLevel":["VP"],"country":["US"],"emailStatus":["verified"],"emailType":["work"]}' \
  | jq '.meta | {total, totalIsExact, distinctCompanies}'

Then run the same request with the statuses you refuse (invalid, risky, disposable, spam_trap, abuse) to see what a plain export would have put in your sequence. From the MCP server, the free tool is leadocean_count_leads. Pass emailStatus yourself: the default counts verified, catch_all_valid and catch_all.

Pull rows only with the status you chose:

json
{
  "jobLevel": ["VP"],
  "country": ["US"],
  "emailStatus": ["verified"],
  "emailType": ["work"],
  "limit": 25
}

emailType: work means the address is at the person's current employer. LeadOcean does not refund or credit back bounced emails, so read the status before you send.

Step 4: Re-check addresses that bounced once

A soft bounce can be a full inbox. It can also be a person who left. Ask who holds the address now with a reverse lookup.

bash
curl -s "https://api.leadocean.io/v2/people/reverse?email=jane.doe@acme.com" \
  -H "x-api-key: $LEADOCEAN_API_KEY" | jq .

A 404 means no person holds the address, so suppress it. A hit returns the person, plus the address's own type, status and check date. If the employer is not Acme, the person changed jobs and the address is stale. Each call costs one record, found or not, so run it on the soft-bounce group only.

To find where the person went, enrich by linkedin_url or person_id:

bash
curl -s -X POST https://api.leadocean.io/v1/people/enrich \
  -H "x-api-key: $LEADOCEAN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"person_id":"1234567890","reveal_email":true}' \
  | jq '.data.contact_data.contact_emails[] | {email, type, status, verified_batch_date}'

An enrichment that finds nothing is a 404 and costs nothing.

Step 5: Track the rate and route by status

Compute the bounce rate per send, not per month. One bad list is visible in one send and hidden in a monthly average.

python
def bounce_rate(sent: int, hard: int, soft: int) -> dict:
    return {
        "hard_pct": round(100 * hard / sent, 2),
        "soft_pct": round(100 * soft / sent, 2),
    }

STATUS_ROUTE = {
    "verified": "send",
    "catch_all_valid": "send, watch",
    "catch_all": "separate batch",
    "risky": "hold",
    "unknown": "hold",
    "untested": "hold",
    "role": "hold",
    "invalid": "drop",
    "disposable": "drop",
    "spam_trap": "drop",
    "abuse": "drop",
}

Set your own ceiling for hard bounces and stop a send that crosses it. See email bounce rate for how to read the number and soft bounce email for retry timing. Why emails bounce covers the causes. The wider workflow is in the deliverability guide.

What you get

Counts below were run on 2026-10-21 with the free leadocean_count_leads tool for jobLevel VP in the US. No rows were returned and no records were spent.

FilterPeople
hasEmail: true (any status)2,840,793
emailStatus: verified only257,840
emailStatus: invalid, risky, disposable, spam_trap, abuse354,526

These are counts by the status you filtered, not mailable counts. The refused group is larger than the verified group. That is the case for gating on status at the source.

Enrichment data object. Response shape from the API reference (openapi.json, October 2026), placeholder values.

json
{
  "contact_data": {
    "has_email": true,
    "email_status": "verified",
    "email_type": "work",
    "contact_emails": [
      {
        "email": "jane.doe@acme.com",
        "type": "work",
        "status": "verified",
        "verified_batch_date": "2026-09-20"
      }
    ]
  }
}

Troubleshooting

ErrorCauseFix
401Missing or wrong x-api-key headerRe-export $LEADOCEAN_API_KEY and check the header name
429Above the plan's rate limit: 5 requests a second on Free, 200 on AgencyWait for the Retry-After value, then retry
402The 1,000 free records are spentMove to Solo ($97 a month) or Agency ($497 a month)
Empty resultsA filter value is not an exact enum string, such as vp in jobLevelList values with /v1/enums/job_level and copy one
404 on reverse or enrichNobody holds that address or identifierSuppress the address. A 404 costs nothing on enrich
meta.depthCapped is trueOne search pages through 10,000 rows at mostSplit the search into narrower filters

FAQ

What is the difference between a bounce checker and bounce management?

A checker scans addresses once and reports failures. Management is the loop after it: classify, suppress, gate the next list and track the rate per send.

Is a hard bounce ever worth retrying?

No. A 5.x.x code is permanent for that address. Retrying adds bounces and hurts your sender reputation.

How many soft bounces before I suppress an address?

Three is a sound default, as in Step 2. Change it if your sender retries on its own, so you do not count the same message twice.

Does LeadOcean refund bounced emails?

No. There is no refund or credit-back for bounces. Gate on emailStatus first and re-check suspect addresses at enrichment.

Which plan do I need?

Free covers this walkthrough. Solo is $97 a month for one person, and Agency is $497 a month for teams. See pricing.

Stop bounces before the send. Filter by email status for free.

Free to start. No credit card. 1,000 records to spend whenever you like.

Get your free API key →