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

How-to guide

How to Test B2B Data Accuracy (Before You Pick a Lead Gen Brand)

Every vendor claims excellent accuracy. Here is a 50-row test you can run on any of them, with real queries and the numbers to record.

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

You cannot rank B2B data brands by accuracy from a comparison page. You can test them: pull the same slice from each, send it, and count bounces and wrong people. This guide runs that test against LeadOcean in 45 minutes.

Examples use placeholders (Acme, acme.com, Jane Doe). Swap in your own filters.

TL;DR

  • Accuracy is a number you measure on your own segment. Vendor claims are not comparable, because each one defines "verified" differently.
  • Take a 50-row sample per vendor, gate on the same email status, then record bounce rate and wrong-person rate.
  • This takes about 45 minutes of work and spends 100 LeadOcean records of the 1,000 free ones: 50 search rows and 50 enrichments. Counts and enum lookups are free.

Prerequisites

  • A LeadOcean key from app.leadocean.io. The Free plan works: no card, 1,000 records for life.
  • The key in your shell: export LEADOCEAN_API_KEY=...
  • curl and jq.
  • A sending domain you can afford to bounce on, or an email verifier you already trust. Do not test on your main domain.
  • The same sample pulled from each vendor you compare. Use their own exports or APIs.

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

Step 1: Fix the segment and the definition

Accuracy differs by country, seniority and industry, so test the segment you will actually sell into. Write it down first. Then write down what counts as an error.

Use three error types. A hard bounce is an address that does not exist. A wrong person is a row whose name or employer no longer matches reality. A stale title is a role that changed. Log each separately, because vendors fail in different ways.

Enum values are exact strings, so look them up before you filter. This call is free.

bash
curl -s "https://api.leadocean.io/v1/enums/job_level" \
  -H "x-api-key: $LEADOCEAN_API_KEY"

VP is a job level. vp is a seniority. Mixing them returns nothing, not an error.

Step 2: Size the segment for free

A count tells you the segment is real before you spend anything. Send count=true as a query parameter with limit=1. It returns 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"],"jobFunction":["Sales & Business Development"],"country":["US"]}' \
  | jq '.meta | {total, totalIsExact, distinctCompanies}'

REST meta.total stops at 100,000, so a larger segment reads as a floor with totalIsExact false. The free MCP tool leadocean_count_leads gave the figures under What you get. Its default counts only people with a verified, catch_all_valid or catch_all email, so label the number as mailable, not as everyone.

Step 3: Pull 50 rows with the status gate applied

Ask for the status you will accept in the query. If a vendor cannot filter by status before you pay, note that: it is a finding.

bash
curl -s -X POST "https://api.leadocean.io/v1/people/search" \
  -H "x-api-key: $LEADOCEAN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "jobLevel": ["VP"],
    "jobFunction": ["Sales & Business Development"],
    "country": ["US"],
    "emailStatus": ["verified"],
    "emailType": ["work"],
    "limit": 50
  }' > sample.json

emailType: work is an address at the current employer. work_other is a former employer's address, so leave it out. A search page of 50 is 50 records.

Do not hand-pick rows. Take the first page, or page with meta.nextCursor and sample every Nth row. Pass the cursor back unchanged.

Step 4: Send the sample and wait

Verified is a claim made at a point in time. A send is the check that matters. Search rows carry the status but not the address, so check the status mix first. Then enrich each row by person_id with reveal_email, one record each.

bash
jq -r '.data[] | [.contact_data.email_status, .contact_data.email_type] | @tsv' sample.json | sort | uniq -c

jq -r '.data[].meta.source_ids.person_id' sample.json | while read -r PID; 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\":\"$PID\",\"reveal_email\":true}"
done | jq -s '.' > enriched.json

Load the 50 addresses into a throwaway domain's sequencer, or run them through your own verifier, and wait 48 to 72 hours. LeadOcean does not refund bounces, so this is also how you learn what a bounce costs you before volume.

For catch-all rows, a verifier cannot prove the mailbox exists. Count them separately from verified rows, or they will flatter the result.

Step 5: Check identity on a handful of rows

Bounce rate misses wrong-person errors, because a stale mailbox can still accept mail. Pick 10 of the enriched rows and compare the current employer with the person's public profile. Reading the file you already have costs nothing.

bash
jq '.[:10][] | .data | {
  role: .contact_data.contact_current_experiences[0] | {job_title, company_name},
  emails: [.contact_data.contact_emails[] | {email, status, verified_batch_date}]
}' enriched.json

Each address carries status and verified_batch_date. The date is a batch date, not a timestamp for that address. The dataset refreshes monthly, and each record carries its own fetched_at. Compare the date with the age of the row: an old date on a senior role is where stale data hides.

Step 6: Score and compare

Put the same four numbers in one table per vendor. Same segment, same sample size, same status gate.

bash
BOUNCED=$(wc -l < bounces.txt)
SENT=50
echo "hard bounce rate: $(echo "scale=1; 100*$BOUNCED/$SENT" | bc)%"

Put the file names in your own notes: bounces.txt holds one bounced address per line, and sample.json holds the 50 rows.

Record: hard bounce rate, share of catch-all rows, wrong-person count out of 10, and what the 50 rows cost (100 records on LeadOcean). With 50 rows the error bars are wide. A 2-row difference is noise. A 10-row difference is a signal.

Step 7: Read the result

Treat a bounce rate under 2 percent as clean for cold outreach, and anything above 5 percent as a reason to stop. Those are common sender-reputation thresholds, not LeadOcean limits, so check them against your sequencer's own guidance.

Look at where the errors cluster before you blame the vendor. If they sit in one country, the vendor is thin there. If they sit in catch_all rows, the gate was too loose. If they sit in rows with an old verified_batch_date, the data is old, not wrong at source.

Then rerun the test with a stricter gate. Move from the default mailable set to verified only and see what the bounce rate does. The cost is the size of the list: in the counts below, verified only keeps under a tenth of the people with no email filter. Decide whether the lower error rate is worth the smaller list.

Run the test twice, a month apart. The dataset refreshes monthly, so a vendor that decays fast shows it on the second run. One run measures accuracy. Two runs measure how quickly it falls.

What you get

Counts run on 2026-10-21 with the free leadocean_count_leads tool for jobLevel VP, jobFunction Sales & Business Development, country US. No rows were returned.

FilterPeople
No email filter (totalAll)350,517
Default mailable (verified, catch_all_valid, catch_all)129,494
emailStatus: verified only31,141

The same segment shrinks from 350,517 people to 31,141 when you accept only verified. Accuracy claims that do not say which of these three numbers they describe are not comparable.

Response shape from the API reference, placeholder values:

json
{
  "success": true,
  "data": {
    "contact_data": {
      "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 spentUpgrade 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 the values with /v1/enums/job_level and copy one
meta.depthCapped is trueOne search pages through 10,000 rows at mostNarrow the filters instead of paging further
Export refusedA request is over 50,000 rows, or the day is over 500,000Split the export, or wait for the next day

FAQ

Which B2B lead gen brands have excellent data accuracy?

None can be named from the outside. Accuracy depends on the country, the seniority and the status you accept. Run this test on two or three vendors and pick on your numbers.

How many rows do I need for a fair test?

Fifty is enough to catch a bad vendor, not to rank two good ones. Use 200 rows when the top two are close. At 200 rows the cost on LeadOcean is 400 records: 200 search rows and 200 enrichments.

Is verified a guarantee against bounces?

No. It means the address passed a check. Mailboxes change after the check, which is why Step 5 reads the batch date and Step 4 sends.

Why separate catch-all addresses?

A catch-all domain accepts any address, so a check cannot prove the mailbox exists. Mixing them with verified rows hides the real bounce rate.

Test our data on your own segment

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

Get your free API key →