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

How-to guide

How to Build a B2B Contact List (With the LeadOcean API)

Size the list for free, pull a sample, check email status, then export. Real requests, a live count and the record cost of each step.

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

A B2B contact list is a set of people at companies you want to sell to, with a job title and a deliverable work email for each. This guide builds one with filters, not a scraped spreadsheet. You size it first, because sizing costs nothing.

TL;DR

  • Define the list as filters (title level, function, country, company size, industry), then count it before you pull a single row.
  • Pull a 25-row sample, read email_status, then export the full list as CSV.
  • Takes about 20 minutes. Spends about 26 records on the Free plan (1,000 records for life, no card). Counts spend none.

Prerequisites

  • A LeadOcean API key from app.leadocean.io, stored as $LEADOCEAN_API_KEY. The Free plan includes search and enrich scopes.
  • curl and jq.
  • A written ICP: who you sell to, in one line. Ours below is VP-level sales leaders at 51-200 person US software companies.

Step 1: Write the ICP as filters

Filters are closed lists, so a guessed value returns nothing. Check exact strings first. The enum calls are free.

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

The ICP becomes this flat JSON body. Filters are arrays of strings.

json
{
  "jobLevel": ["VP"],
  "jobFunction": ["Sales & Business Development"],
  "country": ["US"],
  "employeeRange": ["51-200"],
  "industry": ["Software Development"]
}

The MCP equivalent is leadocean_list_enum_values. There are 50 people filters and 34 company filters in total.

Step 2: Count the list for free

Send count=true as a query parameter and limit 1 in the body. It returns the total and no rows.

bash
curl -s -X POST "https://api.leadocean.io/v1/people/search?count=true" \
  -H "x-api-key: $LEADOCEAN_API_KEY" -H "Content-Type: application/json" \
  -d '{"jobLevel":["VP"],"jobFunction":["Sales & Business Development"],"country":["US"],"employeeRange":["51-200"],"industry":["Software Development"],"limit":1}'

The same count through MCP is leadocean_count_leads. Count the companies with GET /v1/companies/count or leadocean_count_companies. A count that is too small means loosen one filter. A count in the millions means add one.

Step 3: Pull a 25-row sample

Pull a small page before you commit to the full list. This spends 25 records.

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"],"employeeRange":["51-200"],"industry":["Software Development"],"limit":25}' \
  | jq '.meta, (.data | length)'

limit takes 1 to 100. To page, send the nextCursor from meta as cursor in the next request. One search pages to 10,000 rows. If meta.depthCapped is true, narrow the filters instead of walking further.

Step 4: Filter on email status

Email status decides whether a list is sendable. LeadOcean has 13 values. The count tool treats verified, catch_all_valid and catch_all as mailable by default.

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

verified is the strict cut. Adding it shrinks the list, and that is the point. There is no refund for bounced emails, so read email_status before you send.

Step 5: Enrich the rows you keep

Search gives you the row. Enrich gives you the address. Send exactly one of linkedin_url, email, phone or person_id, with reveal_email set to true.

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":"PERSON_ID_FROM_SEARCH","reveal_email":true}'

An enrichment that finds nothing is a 404 and costs nothing. reveal_email also returns phones we hold. For phones alone, use GET /v2/people/phone.

Step 6: Export the full list

For more than a few hundred rows, export instead of paging. The body takes the same filters plus limit.

bash
curl -s -X POST "https://api.leadocean.io/v1/exports" \
  -H "x-api-key: $LEADOCEAN_API_KEY" -H "Content-Type: application/json" \
  -d '{"name":"vp-sales-us-software","limit":5000,"preset":"full","filters":{"jobLevel":["VP"],"jobFunction":["Sales & Business Development"],"country":["US"],"employeeRange":["51-200"],"industry":["Software Development"],"emailStatus":["verified"]}}'

It returns 202 and runs async. Poll GET /v1/exports/{id}, then fetch GET /v1/exports/{id}/download. A request takes up to 50,000 rows, and an account up to 500,000 a day. Bulk CSV export is not on Free, so it needs Solo ($97 a month) or higher. The same export runs from the Exports page in app.leadocean.io: filter builder, column picker, price preview, CSV download.

Step 7: Keep the list clean

A list is a query, not a file. Save the filter JSON in your repo and rebuild when you need a send. Three habits keep it healthy.

  • Exclude what you already contacted. Use excludeCompanyName and excludeTitle for competitors, agencies and interns.
  • Re-run the count each month. The dataset is refreshed monthly, so the total moves.
  • Keep one list per ICP. A VP Sales list and a RevOps list need different messages, so they should be different exports.
json
{
  "jobLevel": ["VP"],
  "country": ["US"],
  "excludeTitle": ["assistant", "intern"],
  "excludeCompanyName": ["Acme"]
}

What each step costs

StepCallRecords
1. EnumsGET /v1/enums0
2. Countcount=true, limit=10
3. Samplesearch, limit 2525
4. Status filtersearch, emailStatus1 per row returned
5. EnrichPOST /v1/people/enrich1 per person found
6. ExportPOST /v1/exports1 per row written

One record is one person or one company you receive. A search page of 25 is 25 records. On Free, 1,000 records covers the 25-row sample, a few dozen enrichments and more search pages. The CSV export needs Solo or higher.

What you get

A live count for the Step 1 ICP, run on 2026-10-21 with the free MCP tool leadocean_count_leads:

json
{
  "total": 1649,
  "totalIsExact": true,
  "totalAll": 4141,
  "distinctCompanies": 1125
}

That is 1,649 people with a mailable address (verified, catch_all_valid or catch_all), out of 4,141 matching people with no email condition, across 1,125 companies. The company side, from leadocean_count_companies on 2026-10-21: 9,261 US-headquartered companies in Software Development with 51-200 employees.

A person record has this shape (response shape from the API reference, placeholder values):

json
{
  "profile_data": { "profile_full_name": "Jane Doe" },
  "contact_data": {
    "has_email": true,
    "email_status": "verified",
    "email_type": "work",
    "contact_emails": [{ "email": "jane@acme.com", "status": "verified" }]
  },
  "meta": {}
}

contact_emails appears only when reveal_email is true.

Troubleshooting

ErrorCauseFix
401Missing or wrong x-api-keyCopy the key from app.leadocean.io into $LEADOCEAN_API_KEY
429Above the plan's rate limit (5 a second on Free, 200 on Agency), or an export ceilingHonor Retry-After. For exports, wait for the daily reset
402Out of records (Free is 1,000 for life)Check GET /v1/usage, then move to a paid plan
Empty resultsA filter value that is not an exact enum stringLook it up with /v1/enums or leadocean_list_enum_values
400 on exportOver 50,000 rows, or a filter the export cannot applyLower limit, or split the list into two exports
Total looks smallThe default count is mailable people onlyCompare with totalAll, or pass emailStatus yourself

FAQ

How big should a B2B contact list be?

Start with the smallest list you can write to this week. A few hundred well-matched rows beats 50,000 loose ones. Count first and tighten the filters until the number fits.

Is a purchased list the same thing?

No. A static file starts decaying the day you buy it. The dataset here is refreshed monthly and each record carries its own fetched_at date, so rebuild the list from filters when you need it.

What does the list cost on the free plan?

Counts, enum lookups and empty enrichments are free. The walkthrough above spends about 26 records of your 1,000. Bulk CSV export starts on Solo at $97 a month. See pricing.

Can I add phone numbers?

Yes. Use GET /v2/people/phone per person, or the full export preset, which carries two phone columns by default.

Where does warm-up fit?

After the list, before the first send. A clean list protects a new domain, and a new domain still needs a ramp. See inbox warming, inbox warm-up and Gmail warmup. More guides live in the blog.

Count your ICP before you buy a single row

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

Get your free API key →