Email bounce rate is bounced sends divided by total sends. For cold email you want hard bounces under 1%, with 2% as the line where you stop and fix the list. Most of the fix happens before the send, at the data source.
This guide measures your current rate, then uses email_status to size, filter and export a list that bounces less. Then it audits the next send so you learn which statuses bounce for you.
Related: what is a good bounce rate, why emails bounce, the full list-cleaning walkthrough and the pricing page. The hub for this section is the LeadOcean blog.
TL;DR
- Bounce rate is bounces divided by sends. Track hard and soft bounces separately, because only hard bounces mean the address is dead.
- Filter on
emailStatusat the source, size the list for free, then export only the statuses you will send to. - The walkthrough takes about 25 minutes and spends about 100 records. Sizing works on Free. The export in step 4 needs Solo ($97 a month) or above.
Prerequisites
- A LeadOcean API key from app.leadocean.io, exported as
$LEADOCEAN_API_KEY. The free plan needs no card. curlandjq, plus Python 3 for steps 1 and 5.- Your last send's bounce report from your sequencer, as a list of bounced addresses. Skip step 1 if you have not sent yet.
Step 1: Measure the rate you have today
The formula is bounced emails divided by emails sent, times 100. Send 1,000 and bounce 30, and your rate is 3%.
Split the count in two. An SMTP 5xx reply is a permanent failure, so it is a hard bounce. A 4xx reply is transient, so it is a soft bounce (RFC 5321, September 2026). Judge your list on the hard rate.
sent = 1000
hard = 22 # 5xx replies: mailbox missing, domain dead
soft = 8 # 4xx replies: mailbox full, server busy
print(f"hard bounce rate: {hard / sent * 100:.1f}%")
print(f"soft bounce rate: {soft / sent * 100:.1f}%")
print(f"total bounce rate: {(hard + soft) / sent * 100:.1f}%")Replace the three numbers with yours. Take them from the same send, and count each address once. A hard rate above about 2% is the usual alarm line for cold outreach, and strong senders run well under 1% (LeadMagic, September 2026). Compute it per mailbox and per list segment too, since one bad segment hides inside a healthy average.
Why bother? Mailbox providers watch how your mail is received, and a sender who keeps writing to dead mailboxes looks like a sender who does not know their recipients. LeadMagic notes that neither Google nor Yahoo publishes a single bounce threshold. So stay well below the alarm line, and treat a climbing rate as a data problem first.
Step 2: Size your list by email status
A sizing call returns a total and no rows, so it costs nothing however often you run it. Pass count=true and limit=1 as query parameters, and change only emailStatus between runs.
for s in verified catch_all untested; do
echo -n "$s: "
curl -s -G https://api.leadocean.io/v1/people/search \
-H "x-api-key: $LEADOCEAN_API_KEY" \
--data-urlencode "jobLevel=VP" \
--data-urlencode "jobFunction=Sales & Business Development" \
--data-urlencode "country=CA" \
--data-urlencode "emailType=work" \
--data-urlencode "emailStatus=$s" \
-d "count=true" -d "limit=1" | jq .meta.total
doneThe same check works from an AI client with the free leadocean_count_leads MCP tool. Ask: "Count VPs of Sales & Business Development in CA with a work email, once each for verified, catch_all and untested."
Run it with your own filters. Title, country and other filters are arrays of strings, so you can add industry or employeeRange the same way. Do not use hasEmail on its own as your mailable count: it includes every status, bad ones too.
Read the gaps between the totals. untested is usually the largest group. It means nobody has checked the address, which is weaker than verified and not the same as bad. meta.total is capped at 100,000, and meta.totalIsExact tells you when the number is a floor.
Step 3: Pick the statuses you will send to
Send verified. Treat catch_all as a separate, smaller batch, because the domain accepts every recipient and no check can confirm one mailbox. Do not send invalid, spam_trap, abuse or disposable. Hold risky, role, derived, untested and unknown.
| email_status | Meaning | Action |
|---|---|---|
verified | LeadOcean's own verifier sent to it and it accepted | Send |
catch_all_valid | Catch-all domain, mailbox confirmed through the provider | Send, watch the rate |
catch_all | The domain accepts everything | Send in a small, separate batch |
risky, role, derived | Shared mailbox, or built from a company pattern and never tested | Hold |
untested, unknown | Not checked, or inconclusive | Hold |
invalid, spam_trap, abuse, disposable | Will bounce or hurt reputation | Drop |
The default sizing in the MCP tool counts verified, catch_all_valid and catch_all unless you pass emailStatus yourself. Say which statuses you filtered whenever you quote a count.
Pair emailStatus with emailType=work. A work_other address sits at a former employer, which is the type most likely to bounce.
Enum values are exact strings. VP works on jobLevel, and vp does not. Check the closed lists with GET /v1/enums/{name}.
Step 4: Export only the safe rows
Export instead of paging search results, because an export writes one CSV and carries the status next to each address. This request takes 100 rows and one email per person.
curl -X POST https://api.leadocean.io/v1/exports \
-H "x-api-key: $LEADOCEAN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "vp-sales-ca-verified",
"filters": {
"jobLevel": ["VP"],
"jobFunction": ["Sales & Business Development"],
"country": ["CA"],
"emailType": ["work"],
"emailStatus": ["verified"]
},
"limit": 100,
"columns": ["person_id", "profile_full_name", "current_company_domain",
"email_N_address", "email_N_status"],
"caps": {"emails": 1}
}'The response is 202 with an export id. Poll GET /v1/exports/{id} until it is done, then fetch GET /v1/exports/{id}/download. The CSV headers come back as email_1_address and email_1_status.
Bulk CSV export is not on the Free plan. It is included from Solo ($97 a month) up. On Free, page the search 100 rows at a time instead. One export takes up to 50,000 rows, and an account takes 500,000 a day. The app's Exports page offers the same filter builder with the record price shown before you start.
Step 5: Audit the next send by status
After the send, join your sequencer's bounce list to the export and compute the rate per status. LeadOcean publishes no bounce rate per status, so this step gives you your own numbers.
import csv, sys
"""usage: python audit.py export.csv bounced.txt"""
export, bounced_file = sys.argv[1], sys.argv[2]
bounced = {line.strip().lower() for line in open(bounced_file) if line.strip()}
sent, hit = {}, {}
with open(export, newline="") as f:
for row in csv.DictReader(f):
email = (row["email_1_address"] or "").lower()
status = row["email_1_status"] or "none"
if not email:
continue
sent[status] = sent.get(status, 0) + 1
hit[status] = hit.get(status, 0) + (email in bounced)
for status, n in sorted(sent.items()):
print(f"{status:<14} sent={n:<6} bounced={hit[status]:<5} rate={hit[status] / n * 100:.1f}%")
print(f"overall rate = {sum(hit.values()) / sum(sent.values()) * 100:.1f}%")bounced.txt holds one address per line, taken from your sequencer's report. Suppress every hard bounce permanently, and review any address that soft-bounces three sends in a row.
If you send catch_all rows, run them as their own batch and compare their rate to verified. A catch-all server may accept the message and bounce it later, so wait a few days before you read the number.
What you get
The sizing loop returns one number per status. These are results from the free leadocean_count_leads MCP tool on 2026-10-21 for VPs in Sales & Business Development in CA, filtered to work addresses. They are counts by emailStatus, not a mailable total.
{
"audience": "jobLevel VP, jobFunction Sales & Business Development, country CA, emailType work",
"date": "2026-10-21",
"counts": {
"verified": 1602,
"catch_all": 2993,
"untested": 5560,
"any email (hasEmail true)": 12318
},
"records_spent": 0
}Sending to everyone with an email would put 12,318 addresses in the list, and only 1,602 of them are verified. That gap is where avoidable bounces come from.
The export row shape below follows the column list from the API reference, with placeholder values.
person_id,profile_full_name,current_company_domain,email_1_address,email_1_status
123456789,Jane Doe,acme.com,jane.doe@acme.com,verifiedTwo habits keep the number down between sends. Re-run step 2 before every campaign, because the dataset is refreshed monthly and each record carries its own fetched_at date. And keep one suppression list across all your mailboxes, so a hard bounce on one sender is never retried from another.
Troubleshooting
| Error | Cause | Fix |
|---|---|---|
| 401 | Missing or wrong x-api-key header | Check $LEADOCEAN_API_KEY is exported and the header is lowercase x-api-key. |
| 402 | Out of records. The free plan's 1,000 are a one-off allowance | Size with count=true first and lower the export limit, or move to Solo ($97 a month) or Agency ($497 a month). |
| 429 | Above your plan's rate limit per account (5 a second on Free, 200 on Agency) | Wait the Retry-After seconds, slow the loop in step 2 and retry. |
| Empty results on a filter that should match | A bad enum value, such as vp for VP | Look up the exact string with GET /v1/enums/{name} or the free list_enum_values tool. |
400 export_limit_rows | More than 50,000 rows in one export | Split the export by country or seniority. |
429 export_limit_daily | Over 500,000 export rows in a day | Run the remainder after the daily reset. |
FAQ
What is a good email bounce rate?
For cold email, keep hard bounces under 1%, and stop to fix the list at 2%. Newsletters and transactional mail should sit lower still. The good bounce rate guide has the targets by send type.
What is the difference between a hard and a soft bounce?
A hard bounce is a permanent failure, usually a 5xx reply, and means the mailbox does not exist. A soft bounce is temporary, a 4xx reply such as a full mailbox. Why emails bounce covers the causes.
Does LeadOcean refund bounced emails?
No. There is no refund or credit-back for bounces. Read email_status before you send, which is why steps 2 to 4 filter first.
Is the search status or the enrichment status correct?
Enrichment. The search index has no per-address verification record, so a verified count from search is an upper bound. For a short list, enrich by person_id with reveal_email and read the status again before sending.
How many records does this walkthrough cost?
About 100. Sizing is free, and the export costs one record per row. The audit script makes no API calls.
Cut your bounce rate before you send
Free to start. No credit card. 1,000 records to spend whenever you like.
Get your free API key →