TL;DR
- Response rate follows list quality. Narrow the ICP, keep only mailable emails, and send small batches so you can read the results.
- The recipe takes about 15 minutes and spends about 25 of your 1,000 free records. Counts are free.
- You end with a sized ICP, a 25-row pull and a rule for when to widen or stop.
Prerequisites
- A LeadOcean account and API key from app.leadocean.io. The free plan has no card and 1,000 records, one-off.
- The key in your shell as
$LEADOCEAN_API_KEY. curlandjq.
export LEADOCEAN_API_KEY="paste-your-key-here"Step 1: Write the ICP as filters, not adjectives
A good ICP is a query. "Mid-market sales leaders" is a mood. jobLevel VP, jobFunction Sales & Business Development, country US, employeeRange 51-200 is something you can run and count.
Enum values are exact strings, so look them up before you trust a zero. The REST route is GET /v1/enums/{name}, and the MCP tool is leadocean_list_enum_values.
curl -s "https://api.leadocean.io/v1/enums/job_level" \
-H "x-api-key: $LEADOCEAN_API_KEY" | jqJob levels are C-Team, VP, Director, Manager, Staff and Other. Seniority is lowercase (c_suite, vp, director).
Step 2: Size it for free
Count before you pull. A count spends no records, however often you run it. Send count=true as a query parameter, with limit at 1.
curl -s "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"],
"emailStatus": ["verified"],
"limit": 1
}' | jq '.meta | {total, totalIsExact, countOnly}'meta.total is capped at 100,000. If your total is far above what one rep can work in a month, the ICP is too wide. Add a filter. A list you cannot personalise is a list that does not reply.
Step 3: Keep only mailable rows
Bounces and spam traps hurt your sender reputation, and a damaged domain lowers every later reply rate. Filter on emailStatus at pull time. LeadOcean does not refund or credit back bounced emails, so read the status before you send.
| Setting | Counts | Use when |
|---|---|---|
["verified"] | Verified addresses only | Your domain is new or reputation matters most |
["verified", "catch_all_valid", "catch_all"] | The default mailable set | You accept some catch-all risk for volume |
["verified", "risky", "unknown"] | Adds unconfirmed addresses | Do not send to these cold |
The first two rows are the ones worth sending to. Compare the counts and pick on purpose.
Step 4: Pull 25 people and read them
Pull a small batch first. Each person returned costs one record, so 25 rows is a cheap test of whether your title filter matches real buyers. The body is flat JSON, filters are arrays, and limit runs from 1 to 100.
curl -s 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"],
"excludeTitle": ["assistant", "intern", "associate"],
"country": ["US"],
"employeeRange": ["51-200"],
"emailStatus": ["verified"],
"limit": 25
}' > people.json
jq '{rows: (.data | length), credits: .meta.credits, nextCursor: .meta.nextCursor}' people.jsonRead all 25 rows. If more than a few titles are wrong, tighten title and excludeTitle and pull again. Fix the filter, not the copy.
Step 5: Add one reason to write now
A reply needs a reason to reply today. Pick one filterable signal per batch and put it in the first line. Tech stack is the cheapest one: technologies matches companies with a detected tool, and detection covers about 7.8M crawled sites. Treat a match as a strong hint, not a census.
curl -s "https://api.leadocean.io/v1/people/search?count=true" \
-H "x-api-key: $LEADOCEAN_API_KEY" -H "content-type: application/json" \
-d '{
"technologies": ["HubSpot"],
"jobLevel": ["VP"],
"jobFunction": ["Sales & Business Development"],
"country": ["US"],
"emailStatus": ["verified"],
"limit": 1
}' | jq '.meta | {total, distinctCompanies}'Run the count with and without the signal. If the signal list is under a few hundred people, you have a segment worth a custom message. If it is empty, drop the signal and keep the rest of the ICP.
Write the opener from fields you hold: title, company, tech. If a field is empty, leave it out. A wrong detail costs more replies than a plain line.
Step 6: Split into small batches and send
Send 25 to 50 contacts per variant, not 2,000 at once. A small batch lets you see which segment replies, and it protects the domain if the copy misses. Page through the same query with cursor to get the next batch.
CURSOR=$(jq -r '.meta.nextCursor' people.json)
curl -s 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\"],
\"emailStatus\": [\"verified\"],
\"limit\": 25,
\"cursor\": \"$CURSOR\"
}" > batch2.jsonOne search pages to 10,000 rows. Past that, meta.depthCapped is true and there is no nextCursor. Narrow the filters instead of walking the index.
Sending itself happens in your own sequencer. LeadOcean has no sequencer, so export the rows or pipe them into your tool of choice. For an export, POST /v1/exports takes up to 50,000 rows a request and 500,000 a day, and it is not on the Free plan.
Set a stop rule before the first send. If a batch of 50 gets no replies and the bounce rate is above 2 percent, stop. Check the email status filter and the title match before you touch the copy.
Measure by segment: reply rate per title, per company size and per email status. Widen only the segment that replied. For the measurement side, see cold email response rate.
Where most lists go wrong
Three mistakes account for most weak lists. Each one is a filter you can fix before you write a word of copy.
- Titles by keyword. A
titlematch on "sales" returns assistants and interns. UsejobLevelandjobFunctionfor the band, thentitlefor the exact role, and addexcludeTitle. - Company size by guess.
employeeRangeis a fixed band such as51-200. Pick one band per batch so the pain in your message fits the whole batch. - Unchecked email status. The default count only counts verified, catch_all_valid and catch_all addresses. Set
emailStatusyourself so you know what you are sending to.
Fix these in the query, then rerun the free count. The total should drop. That is the goal.
What you get
The count in Step 2 comes back as a meta block. This is a real run of the free leadocean_count_leads MCP tool on 2026-10-21. The filters line is added here as a label.
{
"filters": "jobLevel VP, jobFunction Sales & Business Development, country US, employeeRange 51-200, emailStatus verified",
"total": 4552,
"totalIsExact": true,
"distinctCompanies": 3463,
"credits": 0
}The same filters without employeeRange and with the default mailable statuses (verified, catch_all_valid, catch_all) counted 129,494 US VPs in sales and business development, on the same date. Adding the size band and the verified-only rule cut the list to 4,552. That is a list one team can personalise.
A search row from Step 4 has this shape, with placeholder values. This is the response shape from the API reference.
{
"success": true,
"data": [
{
"profile_data": { "profile_full_name": "Jane Doe", "profile_headline": "VP Sales at Acme" },
"contact_data": {
"has_email": true,
"email_status": "verified",
"contact_current_experiences": [
{ "job_title": "VP Sales", "company_name": "Acme", "company_domain": "acme.com" }
]
},
"meta": { "source_ids": { "person_id": "1234567" } }
}
],
"meta": { "count": 25, "limit": 25, "credits": 25, "nextCursor": "..." }
}Search rows are thin: profile and current role, with email flags rather than addresses. To get an address, enrich by the row's meta.source_ids.person_id with reveal_email set to true. The enrichment is one record, and reveal_email adds nothing on top.
Troubleshooting
| Error | Cause | Fix |
|---|---|---|
| 401 | Missing or wrong x-api-key header | Check $LEADOCEAN_API_KEY is exported and the header name is exact |
| 429 | Above the plan's rate limit (5 a second on Free, 200 on Agency) | Wait for Retry-After, then slow the loop |
| 402 | Out of records on the Free plan | Move to Solo ($97 a month) or Agency ($497 a month), or narrow the pull |
Empty data on a filter that should match | A bad enum value, such as Sales instead of Sales & Business Development | Resolve the value with GET /v1/enums or leadocean_list_enum_values |
meta.depthCapped is true with no nextCursor | One search reached 10,000 rows | Narrow the filters and run a new query |
| Export refused | Over 50,000 rows a request or 500,000 a day, or you are on Free | Split the export, or upgrade (bulk CSV export is not on Free) |
FAQ
What is a good B2B cold email response rate?
LeadOcean does not publish a benchmark, and we will not invent one. Track your own reply rate by segment and compare it with your last send. See cold email response rate for how to measure it.
Does list size or list quality matter more?
Quality. A small list with exact titles and verified emails beats a large list with guessed titles. Count first, then decide how many rows you can actually personalise.
How many records does this recipe spend?
About 25, for one search page of 25 people. Counts, enum lookups and filter lookups are free. Enrichment that finds nothing is free too.
Can I run this from an AI assistant instead of curl?
Yes. LeadOcean has a hosted MCP server with 13 tools, and leadocean_count_leads is free. See prospecting tools with the best integrations for the tools that connect.
Does a rep need a tool for this?
The data side does not. Sending and follow-up live in your sequencer or CRM. For the rep workflow around the data, see rep prospecting. Plans start free and are listed on pricing. The section hub is the blog.
Size your ICP before you send a single email
Free to start. No credit card. 1,000 records to spend whenever you like.
Get your free API key →