Resolve the company to a domain, match the person at that domain, then read their checked work email. LeadOcean does this in two calls and about two records.
A name alone is ambiguous. A name plus a domain is usually enough to pick one person. This guide covers both the manual route and a script you can run on a list.
TL;DR
- Search people by name and company domain, take the
person_id, then enrich it withreveal_email=true. - One person takes about 10 minutes the first time and costs 2 LeadOcean records. A script handles a list at the same cost per row.
- Only send to
verifiedorcatch_all_validaddresses. Everything else is a guess.
Prerequisites
- A LeadOcean API key from app.leadocean.io, exported as
$LEADOCEAN_API_KEY. Free gives 1,000 records one-off, no card. curlandjqfor the single lookup. Python 3 withrequestsfor the bulk script.- The person's full name and the company's website domain (acme.com, not "Acme Inc").
LeadOcean has no name plus domain lookup in one call. You search by domain first, then enrich by person_id.
Step 1: Try the manual way first
The manual way is to guess the address pattern and test it. It works for small companies and fails on catch-all domains, where every guess looks valid.
Build the usual candidates for Jane Doe at acme.com:
first=jane; last=doe; domain=acme.com
printf '%s\n' \
"$first@$domain" \
"$first.$last@$domain" \
"${first:0:1}$last@$domain" \
"${first:0:1}.$last@$domain" \
"$first$last@$domain" \
"$last@$domain"You still need a separate checker to know which one bounces. Skip this when the domain is a catch-all or the company is large, since several patterns usually coexist. A lookup against real records avoids the guessing.
Step 2: Search the person by name and domain
Search with the name as q and the company domain as a filter. Cap limit at 3 so a common name cannot spend more than 3 records.
curl -s -X POST https://api.leadocean.io/v1/people/search \
-H "x-api-key: $LEADOCEAN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"q": "Jane Doe",
"domain": ["acme.com"],
"limit": 3
}' | jq '.data[] | {name: .profile_data.full_name, title: .profile_data.title, person_id: .meta.source_ids.person_id}'Each person returned counts one record. Filters are arrays, the body is flat JSON, and limit runs from 1 to 100. If you only have the company name, filter with "companyName": ["Acme"] instead.
Size it for free first. Send count=true as a query parameter, keep limit at 1 in the body, and read meta.total.
curl -s -X POST "https://api.leadocean.io/v1/people/search?count=true" \
-H "x-api-key: $LEADOCEAN_API_KEY" \
-H "Content-Type: application/json" \
-d '{"q": "Jane Doe", "domain": ["acme.com"], "limit": 1}' | jq '.meta.total'Read the rows before you spend the next record. Compare title, city and company with what you know. If two Jane Does work at acme.com, pick the one whose title fits, or add title or country to the search and run it again.
Step 3: Enrich by person_id and reveal the email
Send the person_id from step 2 to the enrich endpoint with reveal_email set to true. It costs one record.
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": "1234567890", "reveal_email": true}' \
| jq '.data.contact_data.contact_emails'Enrich takes exactly one of linkedin_url, email, phone or person_id. The person_id route exists because search rows publish email flags, not addresses.
Step 4: Pick the address and read its status
Each address comes with a type and a status. Keep type work and status verified or catch_all_valid. Drop the rest.
If you only want one checked work address, skip enrichment and call the work-email endpoint:
curl -s "https://api.leadocean.io/v2/people/email/work?person_id=1234567890" \
-H "x-api-key: $LEADOCEAN_API_KEY" | jq '.data'It returns one current-employer address that was checked, or data: null. A null means no address qualifies, not that the person has none. It costs one record per call, including a miss.
Use enrichment when you want every address and phone held on the record. Use the work-email endpoint when you want one checked address and nothing else. Both resolve to the same cached record, so a person looked up once is answered from cache by the other.
LeadOcean does not refund bounces. Read status before you send.
Step 5: Run it for a list
Loop the same two calls over a CSV with name and domain columns. The rate limit is per plan: 5 requests a second on Free, 50 on Solo, 200 on Agency. The script sleeps 0.4 seconds a row to stay under the Free rate. Shorten it on a paid plan.
import csv, os, time, requests
BASE = "https://api.leadocean.io"
H = {"x-api-key": os.environ["LEADOCEAN_API_KEY"]}
OK = {"verified", "catch_all_valid"}
def find_email(name, domain):
r = requests.post(f"{BASE}/v1/people/search", headers=H,
json={"q": name, "domain": [domain], "limit": 1})
rows = r.json().get("data", []) if r.ok else []
if not rows:
return None
pid = rows[0]["meta"]["source_ids"]["person_id"]
e = requests.post(f"{BASE}/v1/people/enrich", headers=H,
json={"person_id": pid, "reveal_email": True})
if not e.ok:
return None
for m in e.json()["data"]["contact_data"].get("contact_emails", []):
if m.get("type") == "work" and m.get("status") in OK:
return m["email"]
return None
with open("people.csv") as f, open("out.csv", "w", newline="") as g:
w = csv.writer(g)
w.writerow(["name", "domain", "email"])
for row in csv.DictReader(f):
w.writerow([row["name"], row["domain"], find_email(row["name"], row["domain"])])
time.sleep(0.4) # two calls a row, 5 requests a second on FreeWith limit 1 a row costs at most 2 records. A search that matches nobody returns no rows, so it spends no record. A 500-row list costs at most 1,000, which is the whole free allowance. For the CSV workflow in more detail, see find email from a CSV of names.
The script takes the first match because limit is 1. That is safe when the domain is the person's real employer and risky for common names. For those rows, raise limit to 3 and compare titles before you enrich.
Three habits keep a bulk run clean. Write the output after every row so a crash loses nothing. Skip rows that already have an email. Review the rows that returned None by hand, because those are where the domain or the spelling is off.
When the list grows past a few thousand rows, check usage with GET /v1/usage before each run. On Solo ($97 a month) or Agency ($497 a month) the work runs at a fair-use pace, with no per-record price. On Free, plan around the 1,000 records you have.
What you get
Enrich returns the person with their contact points under contact_data. This is the response shape from the API reference, with placeholder values.
{
"success": true,
"data": {
"profile_data": { "full_name": "Jane Doe", "title": "Head of Sales" },
"contact_data": {
"has_email": true,
"email_status": "verified",
"email_type": "work",
"contact_emails": [
{
"email": "jane.doe@acme.com",
"type": "work",
"status": "verified",
"verified_batch_date": "2026-09-20"
}
]
}
},
"meta": { "credits": 1 }
}verified_batch_date is the verification run the status came from. Null means the check was not ours, not that the address is unverified. meta.credits is the number of records counted for this call, not a credit balance: paid plans have a fair-use pace, not credits.
Troubleshooting
| Error | Cause | Fix |
|---|---|---|
| 401 | Missing or wrong x-api-key header | Re-export $LEADOCEAN_API_KEY and send it as x-api-key |
| 429 | Above the plan's rate limit (5 a second on Free, 50 on Solo, 200 on Agency) | Honour the Retry-After header and slow the loop |
| 402 | Records spent. On Free the 1,000 are one-off | Move to Solo ($97 a month) or Agency ($497 a month), see /pricing |
Empty data from search | Name spelled differently, or the domain is not the company's main site | Drop q to a surname, or search the domain alone and scan the rows |
| 404 from enrich | No person with that id, or no identifier we can enrich by | Do not retry. Try another match from the search |
503 no_source | The lookup could not be reached | Retry with backoff |
FAQ
Can I look up an email with only a first name and a company?
Yes, but add a title or country to the search. A first name at a large company matches many people, and each row returned costs a record.
Does LeadOcean guess email patterns?
No. It returns addresses held on the record, with a status for each. Pattern guessing stays a manual fallback, and it is unreliable on catch-all domains.
How do I know the email is safe to send?
Read status. Send to verified and catch_all_valid. Treat catch_all as a risk you accept, and skip risky, unknown and invalid.
What if I have only a company name?
Search companies by name to get the domain, or filter people with companyName. If you have a whole domain list, use find email from a company domain.
Can I do this from Claude or another agent?
Yes. The hosted MCP server at https://api.leadocean.io/mcp exposes 13 tools, including search_leads and get_lead. Both cost one record per person returned.
More how-tos live in the blog hub. Related: find email from a YC company list.
Find the email behind any name and company
Free to start. No credit card. 1,000 records to spend whenever you like.
Get your free API key →