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

How-to guide

How to Build a Winning Prospect List for Cold Calling (With an API)

Filter for people who have a number on file, size the list for free, check one phone lookup, then export a call sheet. Real queries and real counts.

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

TL;DR

  • A call list is a tight filter, a "has a phone number" rule and a mobile-first sort. This page builds all three with curl and a short Python script.
  • It takes about 30 minutes. Sizing costs 0 records. The spot check and one phone lookup spend 26, and the 500-row export spends 500.
  • Free covers sizing, the spot check and phone lookups. Bulk CSV export needs Solo ($97 a month) or Agency ($497 a month).

Prerequisites

  • A LeadOcean account from app.leadocean.io. Free gives you 1,000 records, one-off, no card. Free keys carry both the search and enrich scopes.
  • Your API key exported as $LEADOCEAN_API_KEY.
  • curl, jq and Python 3.
  • Your own do-not-call list or a registry scrub. The API does not publish one.
bash
export LEADOCEAN_API_KEY="paste-your-key-here"
mkdir calllist && cd calllist

Step 1: Write the filter as a file

Keep the list definition in one JSON file so every change is reviewable. A cold calling list needs fewer filters than an email list, because a phone number is the scarce field. Of 693M people in the database, 166M have a phone on file, against 366M with an email.

Each filter earns its place. jobLevel and jobFunction pick the buyer. employeeRange keeps the account size where your offer fits. hasPhone removes anyone a rep cannot dial, so no record is spent on a dead end.

This example targets VP and Director sales leaders at US software companies with 51 to 500 staff. It adds hasPhone so every person on the list has a number on file.

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

Save it as filters.json. Enum values are exact strings, and a guessed value returns zero rows. Resolve any you are unsure of with GET /v1/enums/industry or the free leadocean_list_enum_values MCP tool.

Step 2: Size the list for free

A count is free when you send count=true and limit=1 as query parameters. The answer is meta.total. The body stays a flat JSON object.

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 @filters.json | jq '.meta | {total, totalIsExact}'

Read totalIsExact. If it is false, the total is a floor, because the cap is 100,000. Split the list by country or headcount band before you trust it.

Size the list before you decide how big a call day is. One rep dials a few hundred people a week. A pool of 16,000 is a quarter's work for a small team, so slice it and walk one slice at a time.

Step 3: Spot-check 25 rows

Pull one small page before you commit to the whole list. Search rows are a thin projection with no contact points, and each person returned counts one record.

bash
jq '. + {limit: 25}' filters.json > page1.json
curl -s -X POST "https://api.leadocean.io/v1/people/search" \
  -H "x-api-key: $LEADOCEAN_API_KEY" -H "content-type: application/json" \
  -d @page1.json > sample.json

jq '.meta | {count, credits, nextCursor}' sample.json
jq -c '.data[0]' sample.json

Read the titles and companies. If you see rows you would not call, add excludeTitle, for example ["assistant", "intern"], or tighten employeeRange. Rerun the free count after each change. Only the 25-row page spends records.

Step 4: Check one phone lookup

Phone numbers come from a separate call. GET /v2/people/phone takes a person_id and returns every number we hold, best first. The position in the array is the priority.

bash
PID=$(jq -r '.data[0].meta.source_ids.person_id' sample.json)
curl -s "https://api.leadocean.io/v2/people/phone?person_id=$PID" \
  -H "x-api-key: $LEADOCEAN_API_KEY" | jq

This lookup costs one record per call, including a call that finds nothing. Each number carries only phone and type. The API reference lists mobile, direct and office; treat any other value as unknown.

Use this endpoint for single lookups, such as a rep who wants one more number before dialing. For a list, use the export in the next step.

Step 5: Export the call sheet

POST /v1/exports runs the same filters as people search and writes a CSV. Bulk CSV export is not on Free: it is included on Solo and Agency. It takes up to 50,000 rows a request and 500,000 a day. Every row counts one record, whatever the columns.

This request asks for 500 rows with phones in the file. The reveal value of email_phone is what puts contact points in the CSV.

bash
jq '{name: "us-sales-vps-call-list", filters: ., limit: 500,
     preset: "full", reveal: "email_phone",
     caps: {emails: 1, phones: 2}}' filters.json > export.json

curl -s -X POST "https://api.leadocean.io/v1/exports" \
  -H "x-api-key: $LEADOCEAN_API_KEY" -H "content-type: application/json" \
  -d @export.json | jq '.data | {id, status, requested, pollAfter}'

Poll GET /v1/exports/{id} until status is done, then read downloadUrl and fetch the file. The export reserves up to limit records at once and releases what it does not write.

bash
ID="paste-the-export-id"
curl -s "https://api.leadocean.io/v1/exports/$ID" -H "x-api-key: $LEADOCEAN_API_KEY" \
  | jq -r '.data.downloadUrl' | xargs curl -s -o calls.csv
head -1 calls.csv | tr ',' '\n' | grep -n phone

The last line prints the phone column headers. Numbered columns come back as phone_1_number and phone_2_number.

Step 6: Tier the list and scrub it

A winning call list has a priority order and a do-not-call check. This script drops rows with no number, caps each company at three dials, and writes one file ready to load into whatever your reps dial from.

python
import csv

PHONE = "phone_1_number"
COMPANY = "current_company_domain"
CAP = 3

with open("calls.csv", newline="") as f:
    rows = [r for r in csv.DictReader(f) if r.get(PHONE)]

per_company, keep = {}, []
for r in rows:
    key = r.get(COMPANY, "")
    if per_company.get(key, 0) < CAP:
        per_company[key] = per_company.get(key, 0) + 1
        keep.append(r)

dnc = {x.strip() for x in open("dnc.txt")} if __import__("os").path.exists("dnc.txt") else set()  # your own do-not-call list
keep = [r for r in keep if r[PHONE] not in dnc]

with open("call_sheet.csv", "w", newline="") as out:
    w = csv.DictWriter(out, fieldnames=keep[0].keys())
    w.writeheader()
    w.writerows(keep)
print(f"{len(rows)} with a number, {len(keep)} on the call sheet")

The API does not publish a do-not-call flag. The v2 phone endpoint leaves the field out on purpose, so its absence is not clearance. Scrub every number against your own list and the registries that apply to your calling regions.

The CSV order is the order the export wrote, so the sheet is not ranked. Rank it with your own rule before it reaches a rep. Put your best-fit accounts first, then spread each rep's day across companies instead of three dials in a row at one switchboard.

If mobiles matter more than office lines, call GET /v2/people/phone for your top accounts only. Its type label separates mobile from office, and best is the first number in the array. Keep that spend small, because each call costs a record.

Rebuild the sheet on a schedule, not once. The dataset is refreshed monthly and each record carries its own fetched_at date, so a list older than a month is worth re-pulling.

What you get

This is the real output of leadocean_count_leads on 2026-10-21 for the Step 1 filters. It cost 0 records. Because hasPhone was set, it counts everyone with a number on file, whatever their email status.

json
{
  "total": 16479,
  "totalIsExact": true,
  "distinctCompanies": 4966,
  "credits": 0,
  "facets": {
    "seniority": [
      {"value": "director", "count": 10776},
      {"value": "vp", "count": 5703}
    ],
    "company_size": [
      {"value": "51-200", "count": 9620},
      {"value": "201-500", "count": 6859}
    ]
  }
}

The same filters without hasPhone, counted the same day with the tool's default email rule, return 8,986. That count covers verified, catch_all_valid and catch_all addresses only, so it is the mailable pool, not the callable one. The two numbers answer different questions. Do not subtract one from the other.

This is the response shape from the API reference for the Step 4 phone lookup, with placeholder values.

json
{
  "success": true,
  "data": {
    "person_id": "<numeric id>",
    "found": true,
    "phones": [
      { "phone": "+1 415 555 0133", "type": "mobile" },
      { "phone": "+1 415 555 0134", "type": "office" }
    ],
    "best": { "phone": "+1 415 555 0133", "type": "mobile" }
  }
}

The endpoint returns data and no meta object. Read GET /v1/usage to see what a run cost.

Troubleshooting

ErrorCauseFix
401Missing or wrong x-api-key headerCopy the key again from the app and check that $LEADOCEAN_API_KEY is set in the shell running curl
429Above your plan's rate limit: 5 a second on Free, 50 on Solo, 200 on AgencyWait for Retry-After and retry. Send phone lookups in sequence, not in parallel
402Fewer records left than the call needs. On Free the 1,000 are one-offCheck GET /v1/account. See pricing: Solo is $97 a month, Agency $497
Empty data, no errorAn enum value that is not an exact stringResolve each value with GET /v1/enums/{name} and drop filters one at a time
404 or found: false on /v2/people/phoneWe hold no record, or no number, for that personSkip the row. A found: false answer still costs one record
400 export_limit_rows or 429 export_limit_dailyThe export asked for over 50,000 rows, or passed 500,000 rows in a daySplit the export into smaller requests across days

FAQ

How big should a cold calling list be?

Size it to what your reps can dial before the data ages. A few hundred well-matched people beat 16,000 loose ones. Slice a large pool by headcount band or country and work one slice at a time.

How many records does a call list cost?

Sizing is free. A search row counts one record, a phone lookup one record, and an export one record per row. On Solo ($97 a month) and Agency ($497) there is no per-record price: each plan has a fair-use pace, and past it the key slows rather than stops.

Does LeadOcean tell me if a number is on a do-not-call list?

No. The phone endpoint publishes no do-not-call field. Scrub your list against your own suppression list and the registries that apply where you call.

Can I load the CSV into a dialer?

Yes, if the dialer imports CSV. LeadOcean has no dialer, sequencer or CRM of its own. It supplies the data, and call_sheet.csv is a plain file.

What do I read next?

Start with how to build a targeted prospect list for the filter choices. Then see how to build a prospect list by tech stack to add a technology signal, and 8 enrichment tools that build better prospect lists for the wider field. The rest of the series is on the blog hub.

Size your cold calling list for free, then pull the numbers

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

Get your free API key →