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 outbound list (for a team selling to B2B)

Size the list for free, pull the people, get the emails, export the rows. Four calls, about 50 records.

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

Outbound starts with a list you can defend. It has a size you checked, a filter you can re-run and emails with a status you can read. This guide builds one with the LeadOcean API. The example targets VP and C-level sales leaders at 51 to 200 person US software companies.

TL;DR

  • Four steps: size the list (free), search people, enrich emails, export. About 20 minutes.
  • It spends about 50 records. Sizing, empty enrichments and the dry run cost nothing.
  • The Free plan (1,000 records, no card) covers Steps 1 to 3. Bulk CSV export in Step 4 needs Solo ($97 a month) or higher.

Prerequisites

  • A LeadOcean key from app.leadocean.io/register, stored as $LEADOCEAN_API_KEY.
  • curl and jq.
  • Optional: the hosted MCP server at https://api.leadocean.io/mcp (13 tools), if you would rather size lists from an AI client.
bash
export LEADOCEAN_API_KEY="paste-your-key-here"
curl -s https://api.leadocean.io/v1/account -H "x-api-key: $LEADOCEAN_API_KEY"

Step 1: Size the list before you spend anything

Send the search filters with count=true as a query parameter and limit=1. It returns the total and spends no records. meta.total is capped at 100,000.

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", "C-Team"],
    "jobFunction": ["Sales & Business Development"],
    "country": ["US"],
    "employeeRange": ["51-200"],
    "industry": ["Computer Software"],
    "limit": 1
  }'

Filters are arrays and the values are exact strings. VP and C-Team are valid jobLevel values. vp is not. If a value fails, look it up with the free leadocean_list_enum_values tool or GET /v1/enums.

Through MCP, leadocean_count_leads does the same. On 2026-10-21 it returned this for the filters above:

json
{ "total": 54, "totalIsExact": true, "totalAll": 205, "distinctCompanies": 30 }

By default that total counts only people with a verified, catch_all_valid or catch_all email. totalAll is the same filters with no email condition. Read the gap before you promise a rep a list size.

Step 2: Search the people

Same filters, no count, a real limit. Search takes a flat JSON body and returns 1 to 100 rows a page (default 25). Each person returned is one record.

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", "C-Team"],
    "jobFunction": ["Sales & Business Development"],
    "country": ["US"],
    "employeeRange": ["51-200"],
    "industry": ["Computer Software"],
    "limit": 25
  }' | tee page1.json | jq '.meta'

For the next page, pass meta.nextCursor back as cursor. One search pages to 10,000 rows. Past that, meta.depthCapped is true and there is no cursor. Narrow the filters instead of walking the index.

Step 3: Enrich the emails

Enrichment takes exactly one of linkedin_url, email, phone or person_id. There is no name plus domain lookup, so take person_id from the search page. Set reveal_email to return the address.

bash
PID=$(jq -r '.data[0].meta.source_ids.person_id' page1.json)
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}"

To enrich the whole page, loop over person_id and keep the output.

bash
jq -r '.data[].meta.source_ids.person_id' page1.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}" >> enriched.jsonl
  sleep 0.25
done
wc -l enriched.jsonl

The sleep 0.25 holds you near 4 requests a second, under the Free limit of 5 (Solo allows 50, Agency 200). An enrichment that finds nothing returns 404 and costs nothing.

Since October 2026 the response also carries career history in resume_data: past roles, education, certifications and awards. Past employers have no company_id yet. For phones, call GET /v2/people/phone. It costs one record per call, including a miss.

Step 4: Export the full list

For more than a page or two, export instead of looping. The body takes filters (the search filters, without limit or cursor), a row limit, and a preset or your own columns. Numbered columns are requested as email_N_address, email_N_status and phone_N_number. caps sets how many of each. The CSV headers come back as email_1_address, email_2_address and so on. Bulk CSV export is not on Free. Solo includes it. Agency exports 50k rows a file. The API allows 50,000 rows a request and 500,000 a day.

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": "us-software-sales-leaders",
    "filters": {
      "jobLevel": ["VP", "C-Team"],
      "jobFunction": ["Sales & Business Development"],
      "country": ["US"],
      "employeeRange": ["51-200"],
      "industry": ["Computer Software"]
    },
    "limit": 205,
    "columns": ["profile_full_name", "current_job_title", "current_company_name",
                "current_company_domain", "email_N_address", "email_N_status", "phone_N_number"],
    "caps": { "emails": 2, "phones": 1 }
  }' | jq '.data | {id, status, requested}'

Poll GET /v1/exports/{id} until status is done, then download from downloadUrl. Every row costs one record.

If you would rather not write the body, use the Exports page in the app at app.leadocean.io. It has a filter builder, a column picker and a price preview before you download. The CSV has no sequencer attached. Load it into whatever tool your reps already send from.

Step 5: Split the list by email status and owner

A list is not a campaign. Two rules save most teams a bad first week.

First, cut by status, not by hope. Send to verified first. Treat catch_all as a second wave with a smaller daily cap, and drop risky, unknown, invalid, role, disposable and spam_trap from the first send.

bash
jq -r 'select(.data.contact_data.email_status == "verified")
  | [.data.profile_data.profile_full_name, .data.contact_data.email_status] | @tsv' enriched.jsonl > wave1.tsv

Second, keep the filter, not just the file. Store the JSON body from step 2 in your repo. The dataset refreshes monthly and each record carries its own fetched_at date. Re-run the same body next month and diff the result instead of rebuilding from memory.

For a team, split by owner before you load a sequencer. One rep per country, or one per headcount band, using the same filters with a different array. Each rep gets a list that no one else holds, so two people never email the same VP in the same week.

Step 6: Tighten the filter and re-run

The first count is rarely the right list. Change one filter at a time and recount. Each count is free, so do it ten times.

  • Too few people: widen employeeRange to ["11-50", "51-200", "201-500"], or add "Director" to jobLevel.
  • Too many: add title keywords such as ["Head of Sales", "VP Sales"], or narrow industry to one exact value.
  • Too many catch-all addresses: compare total with totalAll and pass emailStatus: ["verified"] to see the verified-only size.
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", "C-Team", "Director"], "jobFunction": ["Sales & Business Development"],
       "country": ["US"], "employeeRange": ["51-200"], "industry": ["Computer Software"],
       "emailStatus": ["verified"], "limit": 1}' | jq '.meta.total'

A count with emailStatus set is a count of that status only. Label it that way.

Do not pass funding, revenue or HQ city filters to the MCP count tool. It rejects some of them or returns a fake total. Count without them, then add them at search time, where they narrow the list further.

For company-first outbound, size accounts first with GET /v1/companies/count, or the leadocean_count_companies tool. It is free and returns a total with breakdowns by industry, headcount band and HQ country. Then pull the people at those accounts with GET /v1/companies/{domain}/people, which stops at 10,000 rows.

What you get

Step 1 returned the sizing result shown above, on 2026-10-21. Steps 2 and 3 return records. 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": {
        "email_status": "verified",
        "contact_current_experiences": [
          { "job_title": "VP Sales", "company_name": "Acme", "company_domain": "acme.com" }
        ]
      },
      "meta": { "source_ids": { "person_id": "p_0000" }, "fetched_at": "2026-10-01" }
    }
  ],
  "meta": { "count": 1, "nextCursor": "c_0000" }
}

With reveal_email, the enriched record adds contact_data.contact_emails[] and contact_data.contact_phones[], plus career history in resume_data. Filter on email_status before you send. LeadOcean does not refund bounced emails.

Troubleshooting

ErrorCauseFix
401Missing or wrong x-api-key headerRe-copy the key from the app and check $LEADOCEAN_API_KEY is exported
429Requests above the plan's rate limit (5 a second on Free, 200 on Agency)Wait for Retry-After, then slow the loop
402The 1,000 Free records are spentUpgrade, or narrow the filters and spend fewer records
Empty data with a valid queryAn enum value is not an exact string, such as vp for VPCheck GET /v1/enums or leadocean_list_enum_values
meta.depthCapped is trueOne search stopped at 10,000 rowsAdd filters and split the list into smaller searches
Export refusedOver 50,000 rows a request or 500,000 a day, or the plan has no bulk exportSplit the request, or move off Free

FAQ

How many records does a first list cost?

A 25-row search plus 25 enrichments spends about 50. A count, a 404 and an empty page cost 0. See pricing for what each plan includes.

Does LeadOcean send the emails?

No. LeadOcean is the data layer: an API, a hosted MCP server and an Exports page. You send from your own sequencer. For the sending side, read how to automate outbound sales and the outbound automation overview.

Where does this fit in a full outbound stack?

It is the first layer. You still need a sequencer, a mailbox setup and a reply process. LeadOcean covers the list and the contact data. Nothing here replaces a CRM or a dialer.

Why count before searching?

A count is free and a search is not. It also tells you if a filter is too narrow before a rep starts asking why the list is short.

What if I need firmographic filters like industry and headcount?

They are in the same search body, as shown in step 1. For a comparison of sources, see best firmographic data providers for B2B sales teams.

Build your first outbound list with a free API key

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

Get your free API key →