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

How-to guide

How to Measure ROI of a B2B Data Enrichment Tool (With Real Queries)

A five-step test you can run in one afternoon on the free plan, using your own contacts as the yardstick.

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

Enrichment ROI is pipeline from kept records, divided by the cost of every record pulled. Measure it with a small test, not a vendor's headline match rate.

TL;DR

  • Run five steps: size the list, sample it, test fill rate on contacts you already know, price each mailable contact, then track bounces and replies.
  • It takes about 45 minutes of work and a two-week send window.
  • It spends at most 400 of the 1,000 records on the free LeadOcean plan. Counts, breakdowns, empty pages and enrichments that find nothing cost 0.

Prerequisites

  • A LeadOcean account on the free plan. Sign up at app.leadocean.io, no card.
  • An API key exported as $LEADOCEAN_API_KEY.
  • curl, jq and Python 3.
  • A CSV of about 100 contacts you already know well (a CRM export with email and LinkedIn URL). This is your ground truth.
  • A sequencer or mail tool that reports bounces, replies and meetings.

Step 1: Size the list for free

A count tells you how big the addressable list is before you spend a single record. Run it first, and run it with the same filters you will use for the sample.

Send count=true as a query parameter with limit=1. Counts are free, and meta.total is capped at 100,000.

bash
curl -s "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":["CA"]}'

The MCP tool leadocean_count_leads does the same thing from Claude or any MCP client. Note what it counts: by default only addresses worth sending to (verified, catch_all_valid and catch_all). The gap between the mailable total and the all-people total is your first ROI number. It shows how much of the list you could never email.

Step 2: Pull a 200-record sample

A sample shows the shape of the data before you commit. Two pages of 100 spend 200 records.

bash
curl -s "https://api.leadocean.io/v1/people/search" \
  -H "x-api-key: $LEADOCEAN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"jobLevel":["VP"],"country":["CA"],"limit":100}' > page1.json

CURSOR=$(jq -r '.meta.nextCursor // empty' page1.json)

curl -s "https://api.leadocean.io/v1/people/search" \
  -H "x-api-key: $LEADOCEAN_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{\"jobLevel\":[\"VP\"],\"country\":[\"CA\"],\"limit\":100,\"cursor\":\"$CURSOR\"}" > page2.json

Filters are arrays of strings, limit runs 1 to 100, and you page with cursor. Enum values are exact strings: VP, not vp, for jobLevel. Check them with leadocean_list_enum_values (free) before you search.

Step 3: Test fill rate on contacts you already know

Fill rate on your own contacts is the only match rate you can trust. A vendor's figure comes from the vendor's data. Yours comes from your market.

Enrich each known contact by email and compare the answer to what your CRM already holds. Enrich takes exactly one of linkedin_url, email, phone or person_id.

bash
while IFS=, read -r email; do
  curl -s -X POST "https://api.leadocean.io/v1/people/enrich" \
    -H "x-api-key: $LEADOCEAN_API_KEY" \
    -H "Content-Type: application/json" \
    -d "{\"email\":\"$email\",\"reveal_email\":true}" \
    -w "\n%{http_code}\n" >> enrich_out.txt
  sleep 0.25
done < known_contacts.csv

A person LeadOcean holds nothing for returns 404 and costs nothing. So the 404 count is your miss rate, and you pay only for hits. The sleep keeps you under the free plan's 5 requests a second.

Score three things per row:

  1. Found or not (200 versus 404).
  2. Does the job title match your CRM today? Titles older than a few months are the usual miss.
  3. Is the email status one you would send to?

Step 4: Price each mailable contact

Cost per mailable contact is the one number that lets you compare tools. Divide total spend by the contacts you would send to, not by rows returned.

A tool that returns 10,000 rows of which 3,000 are sendable costs more per contact than its price suggests. Always put the sendable count in the denominator.

python
inputs = 'replace with your own measured numbers'
plan_cost_per_month = 97        # Solo list price, or 0 on the free plan
rows_returned       = 1000      # rows you pulled in the month
mailable_share      = 0.78      # share with an email status you would send to
mailable            = rows_returned * mailable_share

cost_per_mailable = plan_cost_per_month / mailable
print(round(cost_per_mailable, 3))

The numbers above are example inputs for the arithmetic, not a LeadOcean measurement. Use your own from Step 3.

Two billing facts change the maths. On LeadOcean a record is one person or company you receive, and misses are free. Paid plans have a fair-use pace instead of credits, so cost per contact falls as you use more of the plan. Read the plan terms on leadocean.io/pricing (checked October 2026) and compare tools on the same basis, as our guide to what data enrichment actually costs explains.

Step 5: Track bounces, replies and meetings

Downstream results are the real return, and they take two weeks to read. Search rows carry email flags, not addresses. So enrich 100 mailable rows from Step 2 by person_id with reveal_email set to true (100 records).

bash
jq -r '.data[] | select(.contact_data.has_email) | .meta.source_ids.person_id' page1.json page2.json \
  | head -100 | while read -r id; do
  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\":\"$id\",\"reveal_email\":true}" \
  | jq -r 'select(.success) | .data.contact_data.contact_emails[0] | [.email, .status] | @csv' >> send_list.csv
  sleep 0.25
done

Send those rows in one campaign and the same number of rows from your current source in another, with the same copy and the same weeks.

Record these per arm after two weeks.

MetricHow to read it
Bounce rateBounced sends divided by sends. This is the cleanest data-quality signal.
Reply rateReplies divided by delivered.
Meetings bookedMeetings divided by delivered.
Cost per meetingTotal spend for the arm divided by meetings.

Then the ROI line.

text
ROI = (meetings x win rate x average deal value - total spend) / total spend

LeadOcean gives no refund or credit-back for bounced emails, so read email_status before you send and exclude risky, unknown and invalid. Count those excluded rows in cost per mailable contact too.

What you get

A free count in Step 1 returns the mailable total, the unfiltered total and breakdowns. This is a real result from leadocean_count_leads with jobLevel VP and country CA, run on 2026-10-21.

json
{
  "total": 47671,
  "totalIsExact": true,
  "totalAll": 155457,
  "distinctCompanies": 16696,
  "credits": 0,
  "defaults": "Counted only addresses worth sending to (emailStatus: verified or catch_all_valid or catch_all)",
  "facets": {
    "industry": [
      {"value": "Financial Services", "count": 6113},
      {"value": "Insurance", "count": 2990}
    ]
  }
}

The mailable share here is 47,671 of 155,457, about 31%. That is the share you can email without choosing a looser status, and it is exactly the gap a headline "match rate" hides.

For the record-level response, this is the response shape from the API reference, with placeholder values.

json
{
  "success": true,
  "data": {
    "profile_data": { "profile_full_name": "Jane Doe" },
    "contact_data": {
      "has_email": true,
      "email_status": "verified",
      "email_type": "work",
      "contact_emails": [
        { "email": "jane.doe@acme.com", "type": "work", "status": "verified" }
      ],
      "contact_current_experiences": [
        { "job_title": "VP of Sales", "company_name": "Acme", "company_domain": "acme.com" }
      ]
    }
  },
  "meta": { "credits": 1 }
}

A search row has the same fields without contact_emails. Its id for a later enrich is at meta.source_ids.person_id, and the page cursor is at meta.nextCursor.

Troubleshooting

ErrorCauseFix
401Missing or wrong x-api-key.Re-export $LEADOCEAN_API_KEY and send it in the x-api-key header.
429Above the plan's rate limit: 5 requests a second on Free, 200 on Agency.Back off for the Retry-After value. Add a sleep to loops.
402Out of records. The free plan holds 1,000 for life.Check GET /v1/usage, then move to a paid plan.
Empty resultsA bad enum value, such as vp where VP is required.List valid values with leadocean_list_enum_values.
Total stops at 100,000meta.total is capped at 100,000.Add filters until the count falls under the cap.
Export refusedA request over 50,000 rows, or past 500,000 rows in a day.Split the export by filter. Bulk CSV export is not on Free.

FAQ

What is a good fill rate for enrichment?

Your own number, measured on contacts you know. A figure measured on a vendor's sample says little about your market, region or seniority mix. Run Step 3 for every tool you compare, with the same 100 contacts.

Should I measure ROI on fill rate or on meetings?

Meetings, with fill rate as the early signal. Fill rate and bounce rate read out within a day. Meetings take weeks, so use the early numbers to stop a bad source quickly.

How many records does the test need?

About 400 of the 1,000 free records for the version above. That is enough to compare bounce rates between two sources roughly, not to prove a 1% difference. For tight comparisons, repeat the test on a paid plan.

Can I run the count step without spending anything?

Yes. A count, a breakdown, an empty page and an enrichment that finds nothing cost 0 records. The leadocean_count_companies tool and GET /v1/companies/count size company lists the same way.

Where do I go next?

Start with what data enrichment is if the vocabulary is new, and read the terminal enrichment guide to script this test. The blog hub lists the rest, and pricing has the plan details.

Measure your own enrichment ROI before you buy

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

Get your free API key →