This stack has three jobs and three tools. LeadOcean builds the list, VerifyHQ checks the emails LeadOcean could not confirm, and ReachHQ sends. Each tool stops where the next begins, and nothing in between needs a person.
TL;DR
- Time: about 45 minutes for a 500-row pilot, then the same scripts run any size.
- Records: about 500 LeadOcean records (one per exported row) on the Free plan's 1,000. Sizing the list costs nothing.
- Cost per 10,000 contacts: about $406 a month in list prices (LeadOcean Solo, VerifyHQ Lite, ReachHQ Scale). ReachHQ's yearly billing takes off 2 months.
Why this split works
Most cold email waste comes from one status: catch_all. A catch-all domain accepts any address, so LeadOcean cannot prove the mailbox exists. Verifying every row again wastes credits on addresses LeadOcean already confirmed. Verifying only the open rows spends them where the answer is still unknown.
The sender is the last step because a bad list damages the sending domain, and a damaged domain is expensive to repair. Put the checks before the send, never after. LeadOcean has 13 email status values, and the split in Step 3 uses four of them. Everything else (risky, invalid, role, disposable, spam_trap) stays out of the export filter, so it never reaches the sender.
Prerequisites
- A LeadOcean account and key at app.leadocean.io. Export it as
$LEADOCEAN_API_KEY. Free needs no card. - A VerifyHQ account and API key, exported as
$VERIFYHQ_API_KEY. New accounts get free credits (confirm the amount at signup, VerifyHQ homepage, October 2026). - A ReachHQ key with the scopes
leads:write,lead_lists:writeandrecipients:write, exported as$REACHHQ_API_KEY(ReachHQ REST API guide, October 2026). ReachHQ's plan comparison marks API access "No" on Solo and the trial and "Yes" on Scale, so budget for Scale (ReachHQ pricing, October 2026). - Python 3 with
requests, and curl.
Step 1: Size the list (free)
Check the audience before you spend a record. A count is free, however many times you ask.
curl -G "https://api.leadocean.io/v1/people/search" \
-H "x-api-key: $LEADOCEAN_API_KEY" \
--data-urlencode "count=true" --data-urlencode "limit=1" \
--data-urlencode 'jobLevel=VP' \
--data-urlencode 'jobFunction=Sales & Business Development' \
--data-urlencode 'country=CA'The response has meta.total and an empty data array. meta.total is capped at 100,000. The same count works over MCP with leadocean_count_leads.
Step 2: Export the list from LeadOcean
Export with emailStatus set to the statuses you want to send, plus catch_all and untested for the rows VerifyHQ will check. Several values in 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":"stack pilot","filters":{"jobLevel":["VP"],"jobFunction":["Sales & Business Development"],"country":["CA"],"emailStatus":["verified","catch_all_valid","catch_all","untested"],"emailType":["work"]},"limit":500,"columns":["person_id","profile_first_name","profile_last_name","current_job_title","current_company_name","current_company_domain","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 leads.csv https://api.leadocean.io/v1/exports/EXPORT_ID/downloadPoll until data.status is done, then download. One request takes up to 50,000 rows, and 500,000 rows a day. Bulk CSV export is not on Free. On a Free key, run the pilot with POST /v1/people/search (limit up to 100, cursor pagination) and write the rows to CSV yourself. Solo includes the export. Prefer clicking? The Exports page at app.leadocean.io has a filter builder, a column picker and the record price before you start.
Step 3: Split by email status
Rows LeadOcean marks verified or catch_all_valid skip the second check. Only catch_all and untested go to VerifyHQ.
import csv
send_to_vhq = ("catch_all", "untested")
keep, check = [], []
with open("leads.csv", newline="") as f:
reader = csv.DictReader(f)
fields = reader.fieldnames
for row in reader:
(check if row["email_1_status"] in send_to_vhq else keep).append(row)
for name, rows in (("keep.csv", keep), ("to_verify.csv", check)):
with open(name, "w", newline="") as f:
w = csv.DictWriter(f, fieldnames=fields)
w.writeheader()
w.writerows(rows)
print(len(keep), "keep,", len(check), "to verify")Step 4: Verify the risky rows in VerifyHQ
Upload to_verify.csv as a bulk file, or call the single-email endpoint for a handful. VerifyHQ's homepage shows this request (VerifyHQ homepage, October 2026).
curl -X POST https://api.verifyhq.io/api/public/verify \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $VERIFYHQ_API_KEY" \
-d '{"email": "jane@acme.com"}'The sample response carries email, result, score, tier, do_not_mail, reason, checks, hosting and domain. VerifyHQ does not publish a rate limit or the full list of result values. Keep rows with do_not_mail false and a score you trust. Its own page suggests sending above 80 and reviewing 50 to 80. Tune that on your bounce data, then append the survivors to keep.csv.
Step 5: Load ReachHQ and attach the list
ReachHQ imports rows with a dry run first, then commits them to a static list (ReachHQ API reference, October 2026).
import csv, os, requests
API = "https://api.reachhq.io/api/v1/leads/import"
HEAD = {"Authorization": f"Bearer {os.environ['REACHHQ_API_KEY']}"}
LIST_ID = os.environ["REACHHQ_LIST_ID"]
rows = [{
"email": r["email_1_address"],
"firstName": r["profile_first_name"],
"lastName": r["profile_last_name"],
"company": r["current_company_name"],
"jobTitle": r["current_job_title"],
"domain": r["current_company_domain"],
} for r in csv.DictReader(open("keep.csv"))]
for i in range(0, len(rows), 500):
body = {"action": "dryRun", "source": "CSV", "fileName": "keep.csv",
"rows": rows[i:i+500]} # switch to commit, add listId, after reading the preview
print(requests.post(API, headers=HEAD, json=body).json())Then attach the list to a campaign. Large lists answer RUNNING, so poll audienceJobStatus.
curl -X POST https://api.reachhq.io/api/v1/campaigns/CAMPAIGN_ID/recipients \
-H "Authorization: Bearer $REACHHQ_API_KEY" \
-H "Content-Type: application/json" \
-d '{"action":"addFromList","leadListId":"LIST_ID"}'ReachHQ allows 600 requests per 60 seconds per credential and answers 429 above it (ReachHQ REST API guide, October 2026). The 500-row chunk is our choice, not a ReachHQ limit. Launching needs a separate campaigns:launch scope, so keep it off this key.
What you get
A live free count on 2026-10-21 for jobLevel VP, jobFunction Sales & Business Development, country CA, using the default mailable filter (verified, catch_all_valid or catch_all emails):
{
"total": 5574,
"totalIsExact": true,
"totalAll": 18800,
"distinctCompanies": 3873,
"credits": 0
}The filter without any email condition matches 18,800 people. credits: 0 confirms the count spent nothing.
One exported row has this shape, with placeholder values (response shape from the API reference):
person_id,profile_first_name,profile_last_name,current_company_domain,email_1_address,email_1_status
p_000123,Jane,Doe,acme.com,jane.doe@acme.com,catch_allCost per 10,000 contacts
Monthly list prices, checked October 2026. Each plan is a flat monthly price, with no per-record charge.
| Step | Tool and plan | Monthly list price | Note |
|---|---|---|---|
| List | LeadOcean Solo | $97 | Bulk CSV export included. Agency is $497 for teams. |
| Verify | VerifyHQ Lite | $9.99 | 10K verification credits a month, for the catch_all and untested subset (VerifyHQ, October 2026) |
| Send | ReachHQ Scale | $299 | API access, 750,000 emails a month, 10 seats (ReachHQ pricing, October 2026) |
| Total | $405.99 | Monthly billing. ReachHQ yearly billing gives 2 months free. VerifyHQ shows a monthly plan and one-time credit packs, no yearly price. |
A 3-step sequence to 10,000 people is 30,000 emails, well inside Scale's limit. That sum is ours. ReachHQ also lists email verification on every plan, so test its check on your catch_all rows before you add VerifyHQ. See pricing for every LeadOcean plan.
Scaling past the pilot
Run the same three scripts with a bigger limit. Check three things before you do.
- Re-run the free count with your final filters, so the export size is a number you chose.
- Confirm the VerifyHQ subset fits your credits. The subset is usually a fraction of the export, so read the
to verifyline the Step 3 script prints. - Run one ReachHQ
dryRunon the full file and read the preview before anycommit.
One search pages to 10,000 rows (meta.depthCapped marks the end), so split a larger market into several filtered exports. A narrower filter also gives a better list than a wider one. Keep person_id in every file so you can join VerifyHQ results back to the LeadOcean rows later.
Where each tool stops
- LeadOcean builds and sizes the list and reports an email status per row. It does not send, verify catch-all mailboxes, or run a sequencer.
- VerifyHQ checks addresses. It does not know your audience and does not publish its rate limit.
- ReachHQ sends and manages the campaign. Its pricing gates API access to Scale.
Troubleshooting
| Error | Cause | Fix |
|---|---|---|
| 401 from LeadOcean | Missing or wrong x-api-key. | Send the key from app.leadocean.io in the x-api-key header. |
| 429 from LeadOcean | Requests above the plan's rate limit (5 a second on Free, 200 on Agency). | Honour Retry-After and slow the loop. |
| 402 on export or search | Records spent. | Lower limit, or move to a paid plan. |
| Empty results | A bad enum value, such as vp for jobLevel. | Check leadocean_list_enum_values. jobLevel is VP, seniority is lowercase. |
| Export refused | Over 50,000 rows a request or 500,000 a day. | Split the export into smaller requests. |
| 401 from ReachHQ | Wrong header. | Use Authorization: Bearer, not x-api-key. |
FAQ
Why verify only some rows?
LeadOcean already marks those rows verified or catch_all_valid. A second check on them adds cost and little information. Spend VerifyHQ credits on catch_all and untested.
Does LeadOcean refund bounced emails?
No. Read email_status before you send.
Can an AI agent run the list step?
Yes. The hosted MCP server at https://api.leadocean.io/mcp has 13 tools. leadocean_count_leads sizes a list for free.
Can I use a different sender?
Yes. The list and verify steps do not change. See the Smartlead and Pipedrive stack, the n8n and Attio stack or the Lemlist and HubSpot stack.
Build your first list, verify, send stack with a free LeadOcean key
Free to start. No credit card. 1,000 records to spend whenever you like.
Get your free API key →