TL;DR
- The reliable route is a database search on title plus an email status, not a guessed pattern. It takes about 15 minutes.
- Sizing the list is free. The test run spends about 27 LeadOcean records, out of the 1,000 on the Free plan.
- Read
email_statusbefore you send. Send toverifiedandcatch_all_validaddresses first.
Head of Product emails come from three places: a data provider, a pattern guess at the company domain, or the person's own profile and site. A provider is the only one that scales. Patterns are a fallback you must verify.
Prerequisites
- A LeadOcean account. The free plan has no card and 1,000 records for life: start free.
- An API key in your shell:
export LEADOCEAN_API_KEY=.... The key never goes in code. curlandjq. Free allows 5 requests a second.- An email sender you control, plus a rule to skip anything that is not verified.
Step 1: Size the list for free
Count before you pull. A count spends no records. Send count=true and limit=1 as query parameters, with the filters in the JSON body.
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 '{"title":["Head of Product"],"salesRegion":["EMEA"],"emailStatus":["verified","catch_all_valid"]}' \
| jq '.meta | {total, totalIsExact, distinctCompanies}'meta.total is the number of matches, capped at 100,000. distinctCompanies says how many employers the list spans. In Claude or another MCP client, call leadocean_count_leads with the same filters.
The title is a keyword match. It can catch neighbours such as "Head of Product Marketing", so scan a sample before you send. Add excludeTitle with ["Marketing"] to drop them.
Step 2: Pull a small sample
Run a 25-row search and keep the email fields. The body is flat JSON and every filter is an array.
curl -s -X POST "https://api.leadocean.io/v1/people/search" \
-H "x-api-key: $LEADOCEAN_API_KEY" \
-H "content-type: application/json" \
-d '{"title":["Head of Product"],"country":["US"],"emailStatus":["verified"],"limit":25}' \
> heads.json
jq '.data[] | {person_id: .meta.source_ids.person_id, email_status: .contact_data.email_status}' heads.jsonEach person returned is one record, so this costs 25. Search rows are thin: they carry has_email and email_status, not the address. Read the titles in the rows before you spend a record on enrichment. Page further with the cursor from the response. One search pages to 10,000 rows, so narrow the filters instead of walking the index.
Step 3: Check the email status
Never send on an address you have not read the status of. LeadOcean returns email_status per address. Bounces are not refunded, so the status is your only guard.
| Status | Send? |
|---|---|
verified | Yes |
catch_all_valid | Yes, watch bounces |
catch_all | Small batches only |
risky, unknown, untested | No |
invalid, role, disposable, spam_trap, abuse | Never |
Filter on this at search time with emailStatus, as in Step 2. Filtering after the fact means you already spent records on rows you will not use.
Step 4: Reveal the address
Send a person_id from Step 2 to enrich with reveal_email set to true. Enrich takes exactly one of linkedin_url, email, phone or person_id. A person we hold nothing for is a 404 and costs nothing.
PID=$(jq -r '.data[0].meta.source_ids.person_id' heads.json)
curl -s -X POST "https://api.leadocean.io/v1/people/enrich" \
-H "x-api-key: $LEADOCEAN_API_KEY" \
-H "content-type: application/json" \
-d "{\"person_id\": \"$PID\", \"reveal_email\": true}" \
| jq '.data.contact_data.contact_emails'For a named person you only know by LinkedIn URL, the v2 work email endpoint returns one current-employer address or null. It costs one record per call, including a call that finds nothing, so use it for named targets only.
curl -s -G "https://api.leadocean.io/v2/people/email/work" \
-H "x-api-key: $LEADOCEAN_API_KEY" \
--data-urlencode "linkedin_url=https://www.linkedin.com/in/jane-doe-example"Step 5: Use a pattern only as a last resort
If nothing returns, try the common patterns at the company domain: first@acme.com, first.last@acme.com, flast@acme.com. Copy the shape from any address you already hold at that domain. A shared inbox such as product@acme.com reaches a team queue, not the person, so it is no substitute.
Then verify each guess with a verifier before sending. A pattern guess without verification is how sending domains get flagged. Keep this to a handful of high-value accounts.
Step 6: Export the full list
Once the sample looks right, export the set. In the app, open Exports at app.leadocean.io, build the filter, pick columns and read the record price before you start. Bulk CSV export is not on Free: it needs Solo ($97 a month) or Agency ($497 a month).
curl -s -X POST "https://api.leadocean.io/v1/exports" \
-H "x-api-key: $LEADOCEAN_API_KEY" \
-H "content-type: application/json" \
-d '{
"name": "head-of-product-noram",
"filters": {"title":["Head of Product"],"salesRegion":["NORAM"],"emailStatus":["verified"]},
"limit": 500,
"preset": "full",
"caps": {"emails": 1}
}'Poll GET /v1/exports/{id} until status is done, then download from GET /v1/exports/{id}/download. The CSV headers include email_1_address and email_1_status. An export takes up to 50,000 rows a request and 500,000 a day.
What you get
On 2026-10-21 we ran the free leadocean_count_leads tool with title "Head of Product" and one salesRegion at a time. The first column is mailable people (verified, catch_all_valid or catch_all, the tool default). The second drops the email condition.
| Region | Mailable Heads of Product | All matches, any email state |
|---|---|---|
| NORAM | 9,438 | 16,957 |
| EMEA | 12,630 | 33,593 |
| APAC | 4,022 | 10,627 |
| LATAM | 891 | 2,243 |
The NORAM result, as the tool returned it.
{
"total": 9438,
"totalIsExact": true,
"totalAll": 16957,
"distinctCompanies": 6242,
"credits": 0
}A revealed person has the shape below. This is the response shape from the API reference, with placeholder values.
{
"success": true,
"data": {
"profile_data": { "...": "name, title, company" },
"contact_data": {
"has_email": true,
"email_status": "verified",
"email_type": "work",
"contact_emails": [
{ "email": "jane.doe@acme.com", "type": "work", "status": "verified" }
]
}
}
}Troubleshooting
| Error | Cause | Fix |
|---|---|---|
| 401 | Missing or wrong x-api-key | Re-export $LEADOCEAN_API_KEY and check for stray spaces |
| 429 | Above the plan's rate limit (5 a second on Free, 200 on Agency) | Honour Retry-After and slow the loop |
| 402 | Out of records. The 1,000 Free records are one-off, not monthly | Move to Solo at $97 a month or Agency at $497 a month |
| Empty results | A filter value is not an exact enum string | Check leadocean_list_enum_values for sales_region and email_status |
| Search stops at 10,000 rows | Depth cap (meta.depthCapped) | Split by country or company size |
| Export refused | Over 50,000 rows a request or 500,000 a day | Split the export into smaller filters |
FAQ
Where do Head of Product emails come from?
From a data provider, a pattern guess at the company domain, or the person's own site. Providers return a status with each address. Patterns and scraped addresses need verification before you send.
Which region has the most mailable Heads of Product?
EMEA, with 12,630 mailable on 2026-10-21, ahead of NORAM at 9,438. NORAM has the higher share with a sendable address: about 56% against 38% in EMEA. Expect a smaller sendable share on European lists.
How many records does this cost?
Sizing is free. The test run spends about 27. A paid plan has a fair-use pace instead of per-record credits.
Does LeadOcean refund bounced emails?
No. Read email_status before sending and start with verified.
Is "Head of Product" the same as VP Product or CPO?
No. Use jobLevel or seniority filters, or add titles such as "VP Product" and "Chief Product Officer" to the title array.
Can I find the email from a name and company alone?
Not in one call. There is no name plus domain lookup. Search by domain first with GET /v1/companies/{domain}/people, then enrich by person_id.
How fresh is the data?
The dataset is refreshed monthly, and each record carries its own fetched_at date. Since 2026-10-17 enrich also returns past roles in resume_data, so you can see how long someone has held the job.
Where do I go next?
See how to find a product manager's email, a Head of RevOps email or a Head of Growth email. More guides are in the blog. Plans are on the pricing page.
Find your Head of Product emails today
Free to start. No credit card. 1,000 records to spend whenever you like.
Get your free API key →