You get a repeatable pipeline. Filter people in LeadOcean, export two CSVs by email status, and load each into its own HeyReach list over the API. HeyReach is a LinkedIn outreach tool, so every lead needs a LinkedIn profile URL. LeadOcean supplies that plus the work email and its status. Catch-all addresses get their own list and campaign, so a bad address never drags down a clean one.
Use this when bounced or dead addresses keep showing up in your LinkedIn campaigns. The fix is to decide per address before anything is sent. LeadOcean tells you which addresses it verified, which sit on catch-all domains and which it could not test. HeyReach then runs each group as its own campaign with its own reporting.
Related: the integrations hub, Smartlead, Reply.io, Salesloft and pricing.
How it works
Two exports, one script, two HeyReach lists. You build it once and rerun it for each new filter. HeyReach takes up to 100 leads per request (HeyReach AddLeadsToListV2, September 2026).
- Get keys. Create a LeadOcean key at app.leadocean.io and a HeyReach API key. Free LeadOcean keys get search and enrich scopes.
- Size the list. A search with
count=trueandlimit=1is free. Check the total before you spend records. - Export verified. Create an export with
emailStatusset toverified. - Export catch-all. Run the same filters again with
emailStatusset tocatch_all_validandcatch_all. - Create two empty lists in HeyReach, one per file, and note each list id.
- Run the script below once per file. It posts leads in batches of 100.
- Attach each list to its own campaign in HeyReach. Put the verified list in your main sequence and the catch-all list in a smaller test campaign.
Keep the two files separate from the start. Merging them later means filtering on email_status again, and a mixed list is how a catch-all address ends up in the main sequence by mistake. Check each campaign's reply and bounce numbers after a week, then decide whether the catch-all list is worth scaling.
Bulk CSV export is not on the Free plan. On Free, call search, then enrich each person with reveal_email set to true to get the address. Contact columns need the enrich scope.
Configuration
The export request. Several values inside one filter join with OR.
curl -X POST https://api.leadocean.io/v1/exports \
-H "x-api-key: $LEADOCEAN_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"heyreach verified","filters":{"jobLevel":["VP"],"jobFunction":["Sales & Business Development"],"country":["CA"],"emailStatus":["verified"],"emailType":["work"]},"limit":1000,"columns":["person_id","profile_first_name","profile_last_name","profile_url","current_job_title","current_company_name","email_N_address","email_N_status"]}'
curl -H "x-api-key: $LEADOCEAN_API_KEY" https://api.leadocean.io/v1/exports/EXPORT_ID
curl -H "x-api-key: $LEADOCEAN_API_KEY" -o verified.csv https://api.leadocean.io/v1/exports/EXPORT_ID/downloadFor the second file, change emailStatus to ["catch_all_valid","catch_all"] and the name to heyreach catch-all. The load script is ours. The 100-lead batch is HeyReach's documented maximum.
import csv, os, sys, requests
URL = "https://api.heyreach.io/api/public/list/AddLeadsToListV2"
HEAD = {"X-API-KEY": os.environ["HEYREACH_API_KEY"]}
LIST_ID = int(sys.argv[2])
leads = []
for r in csv.DictReader(open(sys.argv[1])):
if not r["profile_url"]:
continue # HeyReach requires profileUrl
leads.append({
"firstName": r["profile_first_name"],
"lastName": r["profile_last_name"],
"companyName": r["current_company_name"],
"position": r["current_job_title"],
"emailAddress": r["email_1_address"],
"profileUrl": r["profile_url"],
"location": "", "summary": "", "about": "",
"customUserFields": [
{"name": "email_status", "value": r["email_1_status"]},
{"name": "leadocean_id", "value": r["person_id"]},
],
})
for i in range(0, len(leads), 100):
body = {"listId": LIST_ID, "leads": leads[i:i+100]}
print(requests.post(URL, headers=HEAD, json=body).json())Run python load.py verified.csv 123 where 123 is your list id. HeyReach authenticates with an X-API-KEY header (HeyReach API overview, September 2026).
Field mapping
HeyReach's lead object lists firstName, lastName, location, summary, companyName, position, about, emailAddress, profileUrl and customUserFields, and marks all of them required. The script sends empty strings for fields LeadOcean does not export here.
| LeadOcean field | Target field (HeyReach) | Note |
|---|---|---|
profile_first_name | firstName | Empty if the profile has no split name. |
profile_last_name | lastName | |
profile_url | profileUrl | Rows with no URL are skipped by the script. |
current_job_title | position | Free text, good as a merge field. |
current_company_name | companyName | |
email_1_address | emailAddress | Present only for rows with an email. |
email_1_status | customUserFields.email_status | Name must be letters, digits or underscores. |
person_id | customUserFields.leadocean_id | Lets you re-match a lead later. |
Column ids come from GET /v1/exports/columns. Email status has 13 values. risky, unknown and invalid stay out of both files.
Cost at 10,000 rows
For 10,000 rows, LeadOcean costs Solo at $97 a month, or Agency at $497 a month for teams. HeyReach is priced per LinkedIn sender, not per lead. Its Growth plan shows $79 a month per sender on monthly billing, with 10 percent off quarterly and 20 percent off yearly. Growth includes 100 enrichment credits per sender, API and webhooks (HeyReach pricing, October 2026).
| Item | LeadOcean Free | Solo or Agency |
|---|---|---|
| Records | 1,000 for life, one-off | Fair-use pace, no per-record price |
| Bulk CSV export | Not included | Solo included, Agency 50k rows a file |
| HeyReach | Priced by sender | Priced by sender |
| 10,000 rows | Covers 1,000 | Covers all 10,000 |
An export reserves records up to limit and bills one record per row written. There are no credits and no overage invoice.
Loading is quick on the HeyReach side. 10,000 leads in batches of 100 is 100 requests, and HeyReach allows 300 requests a minute. On the LeadOcean side, one export runs per account at a time, at most 50,000 rows each.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
failedLeadsCount above zero | A required field was missing or a custom field name had a bad character. | Send empty strings, and use only letters, digits and underscores in names. |
| 400 from HeyReach | name in customUserFields is not alphanumeric. | Rename it, for example email_status. |
| Leads sit in the list but never start | Adding to a list does not add to a finished campaign. | Use AddLeadsToCampaignV2, or add the list to an active campaign. |
| 429 from HeyReach | Over 300 requests a minute (HeyReach API overview). | Back off and retry. |
| 402 on export create | Fewer records left than limit reserves. | Lower limit or upgrade. |
| Fewer leads than CSV rows | Rows had no profile_url. | Expected. Filter for people with a LinkedIn URL. |
FAQ
Why split verified and catch-all?
Catch-all domains accept every address, so a send can still bounce. LeadOcean gives no refund for bounces, so read email_status before sending. A small test campaign shows how a domain behaves before you commit the list.
Can I skip the CSV and call HeyReach per lead?
Yes. HeyReach has AddLeadsToCampaignV2, which takes a linkedInAccountId per lead (HeyReach docs, September 2026). Call LeadOcean search first, then post pages of 100.
Do I need Sales Navigator or a LinkedIn search?
No. LeadOcean replaces the lead source. You filter by job level, function, country and email status in LeadOcean, so the list exists before HeyReach sees it. HeyReach still needs a connected LinkedIn sender to run the campaign.
Can I use the MCP server instead?
Yes. LeadOcean's MCP server at https://api.leadocean.io/mcp has an export_leads tool that costs one record per row. HeyReach lists its own MCP server on the Growth plan (HeyReach pricing, October 2026).
Export your first HeyReach list from LeadOcean
Free to start. No credit card. 1,000 records to spend whenever you like.
Get your free API key →