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

How-to guide

How to Measure Email List Decay by Industry (With the LeadOcean API)

Published decay rates are vendor estimates. Take a free status snapshot per industry, track a small panel, and get a rate you can defend.

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

The most-quoted ranges put B2B contact decay at roughly 10% to 70% a year, depending on sector. LeadMagic's guide (September 2026) says plainly that these are vendor estimates, not audited benchmarks.

Two problems for a technical buyer. A vendor figure may count any stale field, including title and phone, not only the email address. And a per-year rate hides what you need: how much of a segment you can still mail next month.

Convert a yearly rate to a monthly one with compounding, not division by 12.

python
annual = 0.35                      # 35% a year
monthly = 1 - (1 - annual) ** (1 / 12)
print(f"{monthly:.1%} a month")    # 3.5% a month

Now measure your own rate. The steps below use three example industries: Software Development, Hospitals and Health Care, and Government Administration.

TL;DR

  • Decay rates by industry are published as vendor estimates, not audited benchmarks. The honest number is the one you measure on your own segments.
  • It takes about 30 minutes today and 10 minutes after 30 days. The panel spends about 900 of your 1,000 free records. The status snapshot spends none.
  • LeadOcean gives you two measures: the email status mix per industry (free counts) and the status change of a fixed panel of people (enrich by person_id).

Prerequisites

  • A LeadOcean API key from app.leadocean.io. The free plan has no card and 1,000 records, one-off. The key needs the search and enrich scopes, which Free keys get.
  • The key in your shell as $LEADOCEAN_API_KEY.
  • curl, jq and python3.
bash
export LEADOCEAN_API_KEY="paste-your-key-here"

Step 1: Resolve the exact industry strings

Industry is a closed list of 534 values. A wrong string returns an empty result, not an error you will notice. Look the values up first. The call is free.

bash
curl -s "https://api.leadocean.io/v1/enums/industry?q=hospital" \
  -H "x-api-key: $LEADOCEAN_API_KEY" | jq '.data.values'

In an MCP client the same lookup is one prompt.

text
Use leadocean_list_enum_values with name "industry" and q "government".
Do not call any tool that spends records.

Pick one value per segment and write it down exactly, for example Hospitals and Health Care.

Step 2: Snapshot the status mix per industry (free)

Count each industry by email status. A search with count=true and limit 1 returns meta.total and no rows, and it costs nothing. This example fixes the audience to US C-Team contacts so the three industries compare like for like.

bash
for IND in "Software Development" "Hospitals and Health Care" "Government Administration"; do
  for BUCKET in '["verified"]' '["catch_all","catch_all_valid"]' '["risky","unknown","untested"]'; do
    TOTAL=$(curl -s "https://api.leadocean.io/v1/people/search?count=true" \
      -H "x-api-key: $LEADOCEAN_API_KEY" -H "content-type: application/json" \
      -d "{\"industry\":[\"$IND\"],\"country\":[\"US\"],\"jobLevel\":[\"C-Team\"],\"emailStatus\":$BUCKET,\"limit\":1}" \
      | jq '.meta.total')
    echo "$(date +%F),$IND,$BUCKET,$TOTAL" >> snapshot.csv
  done
done

Note that REST meta.total is capped at 100,000. If a bucket reaches it, split the audience by employeeRange or country. The free MCP tool leadocean_count_leads is not held to that cap. On 21 October 2026 it returned 178,503 with totalIsExact true.

The dataset is refreshed monthly, so run this loop once a month and keep the file. Each run adds nine dated rows. The change in the mix between runs is your first decay signal, and it costs 0 records.

Step 3: Pull a fixed panel of people

A count shows the mix, not what happened to the same people. For that you need a panel: the same person ids, checked twice. Search 100 verified people per industry and keep their ids.

bash
for IND in "Software Development" "Hospitals and Health Care" "Government Administration"; do
  curl -s https://api.leadocean.io/v1/people/search \
    -H "x-api-key: $LEADOCEAN_API_KEY" -H "content-type: application/json" \
    -d "{\"industry\":[\"$IND\"],\"country\":[\"US\"],\"jobLevel\":[\"C-Team\"],\"emailStatus\":[\"verified\"],\"limit\":100}" \
    | jq -r --arg ind "$IND" '.data[] | [$ind, .meta.source_ids.person_id] | @csv' >> panel.csv
done
wc -l panel.csv

This spends 300 records, one per person returned. Search returns thin rows with no addresses. Each row carries meta.source_ids.person_id, which is the key for the next step.

Search counts verified on a broader rule than enrich does, so treat the search count as an upper bound. The next step shows the true baseline.

Step 4: Record the baseline status by person_id

Enrich each id once and store the status. POST /v1/people/enrich takes exactly one identifier, here person_id. It costs one record per person. A person we hold nothing for is a 404 and costs nothing.

bash
DATE=$(date +%F)
while IFS=, read -r IND PID; do
  PID=${PID//\"/}; IND=${IND//\"/}
  STATUS=$(curl -s https://api.leadocean.io/v1/people/enrich \
    -H "x-api-key: $LEADOCEAN_API_KEY" -H "content-type: application/json" \
    -d "{\"person_id\":\"$PID\"}" | jq -r '.data.contact_data.email_status // "404"')
  echo "$DATE,$IND,$PID,$STATUS" >> baseline.csv
done < panel.csv
cut -d, -f2,4 baseline.csv | sort | uniq -c

Some of the 300 will already read untested or catch_all. That is the search-versus-enrich gap, and it is a real finding about the segment. Drop those from the denominator in Step 6.

Step 5: Re-check the same people after 30 days

Run the same enrich loop on the same ids. Do not re-search: a new search returns different people. Write the result to a second file.

bash
DATE=$(date +%F)
while IFS=, read -r DAY IND PID OLD; do
  STATUS=$(curl -s https://api.leadocean.io/v1/people/enrich \
    -H "x-api-key: $LEADOCEAN_API_KEY" -H "content-type: application/json" \
    -d "{\"person_id\":\"$PID\"}" | jq -r '.data.contact_data.email_status // "404"')
  echo "$DATE,$IND,$PID,$STATUS" >> recheck.csv
done < baseline.csv

This spends up to 300 more records, about 900 in total. Wait at least one monthly refresh before you read the result, because the data changes on that cycle.

Step 6: Compute decay per industry

Your measured rate is the share of people who were verified at baseline and are not verified now. People who left the data entirely (a 404 on the re-check) count as decayed.

python
import csv, collections
base = {r[2]: (r[1], r[3]) for r in csv.reader(open("baseline.csv"))}
now = {r[2]: r[3] for r in csv.reader(open("recheck.csv"))}
ok, lost = collections.Counter(), collections.Counter()
for pid, (ind, s0) in base.items():
    if s0 != "verified": continue
    ok[ind] += 1
    if now.get(pid) != "verified": lost[ind] += 1
for ind in ok:
    print(f"{ind}: {lost[ind]}/{ok[ind]} = {lost[ind]/ok[ind]:.1%} in the window")

Read it as a rate for the window you waited, not a year. Convert with the compounding formula above only if you ran at least 90 days.

Then set the cadence from the rate. Pick the invalid share you will tolerate on a send, and re-verify a segment before its measured loss reaches it. A segment that loses 3% in a month needs a monthly check. One that loses 3% in a quarter needs one a quarter. Keep the per-industry numbers in a table next to the segment tags in your CRM, and re-run the panel twice a year to see whether they drift. A job change moves a person out of the segment, but their old address may keep accepting mail for a while, so status change understates job churn.

What you get

Step 2 is free, so we ran it as MCP counts. These are the US C-Team counts by email status bucket on 21 October 2026. They describe what we filtered, not a mailable audience: the last bucket is risky, unknown and untested addresses.

Industryverifiedcatch_all or catch_all_validrisky, unknown or untestedverified share of the three
Software Development39,06291,448105,97216.5%
Hospitals and Health Care32,924151,508178,5039.1%
Government Administration11,83273,50770,4997.6%

This is one snapshot, so it shows where each industry stands today and no rate. Run the loop again next month, and the difference in the verified share is the first measured movement. Treat the verified counts as an upper bound until you confirm them per address on enrich.

The enrich call in Step 4 and Step 5 returns a person record. This is the response shape from the API reference, with placeholder values.

json
{
  "success": true,
  "data": {
    "profile_data": {},
    "contact_data": {
      "has_email": true,
      "email_status": "verified",
      "email_type": "work"
    },
    "meta": {}
  },
  "meta": {
    "credits": 1,
    "key_hash": "<cache key>"
  }
}

Troubleshooting

ErrorCauseFix
401Missing, wrong or revoked keyRe-copy the key from app.leadocean.io and re-export $LEADOCEAN_API_KEY.
429Above your plan's rate limit per account (5 a second on Free, 200 on Agency)Add a short sleep to the loop, or wait the Retry-After seconds.
402The free 1,000 records are spentCheck GET /v1/account, and lower the panel to 70 people per industry. Free records do not reset.
meta.total is 0 or nullA filter value outside the catalogue, such as industry: "Hospitals"Resolve the exact string with /v1/enums/industry?q= and pass it unchanged.
404 on enrichWe hold nothing for that person_id, or no identifier to enrich byNot billed. Count it as lost in Step 6 and note it separately.
Count stops at 100,000meta.total is capped at 100,000Split the audience by employeeRange or country and add the parts.

FAQ

What is a typical email list decay rate by industry?

Published ranges run from about 10% to 70% a year, and they are vendor estimates. SaaS, startups and recruiting sit at the high end. That tiering is Span Global Services', as cited by LeadMagic (October 2026). Use them to set a first cadence, then replace them with your own measurement.

How is decay different from bounce rate?

Decay is a list-state metric: how many records go bad over time. Bounce rate is a send-time metric: how many messages failed in one campaign. Track both, and read email_status before every send.

Does LeadOcean give me a decay rate directly?

No. LeadOcean returns the current status of each address and the mix per segment. The rate comes from comparing two readings of the same people, as in Steps 3 to 6. No refund or credit is given for bounced emails, so check the status first.

How often does the data change?

The dataset is refreshed monthly, and each record carries its own fetched_at date. A monthly snapshot is the right cadence for Step 2.

Can I run this without code?

Partly. The Exports page in the app at app.leadocean.io has a filter builder, a column picker and the record price before you start. It gives you a CSV of one segment, but the two-reading comparison still needs a script.

For cleaning steps after you measure, read how to clean an email list and cut your bounce rate. For the term itself, see email list churn. For tools that re-verify lists, see the best hygienic email list tools for B2B. Plans are on pricing, and more recipes live in the blog hub.

Measure your own list decay with 1,000 free records

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

Get your free API key →