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,jqand Python 3.- Your own do-not-call list or a registry scrub. The API does not publish one.
export LEADOCEAN_API_KEY="paste-your-key-here"
mkdir calllist && cd calllistStep 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.
{
"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.
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.
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.jsonRead 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.
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" | jqThis 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.
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.
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 phoneThe 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.
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.
{
"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.
{
"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
| Error | Cause | Fix |
|---|---|---|
| 401 | Missing or wrong x-api-key header | Copy the key again from the app and check that $LEADOCEAN_API_KEY is set in the shell running curl |
| 429 | Above your plan's rate limit: 5 a second on Free, 50 on Solo, 200 on Agency | Wait for Retry-After and retry. Send phone lookups in sequence, not in parallel |
| 402 | Fewer records left than the call needs. On Free the 1,000 are one-off | Check GET /v1/account. See pricing: Solo is $97 a month, Agency $497 |
Empty data, no error | An enum value that is not an exact string | Resolve each value with GET /v1/enums/{name} and drop filters one at a time |
404 or found: false on /v2/people/phone | We hold no record, or no number, for that person | Skip the row. A found: false answer still costs one record |
400 export_limit_rows or 429 export_limit_daily | The export asked for over 50,000 rows, or passed 500,000 rows in a day | Split 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 →