Clean the file, match each name to one person, then read that person's checked work email. LeadOcean does the match and the lookup through the API, about 2 records per row that matches.
A CSV with only first and last names will not work. Two Jane Does exist at every large company. You need a second column: a company name or a domain.
TL;DR
- Add a company or domain column, size each row for free, search only the rows that match one person, then call the work-email endpoint.
- A short list takes about 20 minutes end to end. Each matched row costs up to 2 LeadOcean records. A row that matches nobody costs 0.
- Send only to addresses the API returns. It returns
verified,catch_all_validorcatch_allwork addresses, ornull.
Prerequisites
- A LeadOcean API key from app.leadocean.io, exported as
$LEADOCEAN_API_KEY. Free gives 1,000 records one-off, no card, at 5 requests a second. - Python 3 with
requests. - A CSV with the columns
first_name,last_nameandcompany(ordomain). Use real data in your own file. This guide uses Jane Doe at Acme (acme.com) as the placeholder.
LeadOcean has no upload-a-CSV lookup and no name plus domain lookup in one call. The bulk export takes filters, not a list of names. So you loop the API over your rows.
Step 1: Clean the CSV by hand
Fix the file before you spend a record. Most misses come from the input, not the data.
Check four things:
- Split names into
first_nameandlast_name. Drop titles such as "Dr." and suffixes such as "Jr.". - Put one person per row and remove duplicates.
- Add a company column. A domain like
acme.commatches better than "Acme Inc". - Fix spelling and encoding. "Jose" and "José" are different strings to a filter.
A clean file looks like this:
first_name,last_name,company,domain
Jane,Doe,Acme,acme.comThe manual way to a work email is to guess jane.doe@acme.com and test it. That works for a handful of rows. It fails on catch-all domains, where every guess looks valid, so use it only as a fallback.
Step 2: Size one row for free
Count how many people match a row before you search it. A count costs no record.
Send count=true as a query parameter with limit=1. Read meta.total.
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 '{"q": "Jane Doe", "domain": ["acme.com"]}' | jq '.meta.total'Filters are arrays and the body is flat JSON. Read the total like this:
0: nobody matches. Skip the row, or fix the spelling and try again.1: one person. Safe to search.2or more: ambiguous. Add atitleorcountrycolumn, or review it by hand.
Step 3: Search the match and take the person_id
Search the rows that returned a total of 1. Cap limit at 1 so a row cannot spend more than one record here.
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": 1}' \
| jq '.data[] | {name: .profile_data.profile_full_name, headline: .profile_data.profile_headline, person_id: .meta.source_ids.person_id}'Each person returned counts one record. Compare the title and company with what you know before you go on. If you only have a company name, filter with "companyName": ["Acme"] instead of domain.
Step 4: Read the work email
Send the person_id to the work-email endpoint. It returns one address or data: null.
curl -s "https://api.leadocean.io/v2/people/email/work?person_id=1234567890" \
-H "x-api-key: $LEADOCEAN_API_KEY" | jq '.data'The address is at the person's current employer's domain. It was verified, catch_all_valid or catch_all. A null means no address qualifies, not that the person has none. The call costs one record, including a miss.
If you want every email and phone on the record, use POST /v1/people/enrich with reveal_email set to true. That route also costs one record.
Step 5: Loop it over the whole file
Wrap steps 2 to 4 in one script. It reads your CSV and writes a new one with the email, so a crash loses nothing.
import csv, os, time, requests
BASE = "https://api.leadocean.io"
H = {"x-api-key": os.environ["LEADOCEAN_API_KEY"]}
def find_email(first, last, domain):
body = {"q": f"{first} {last}", "domain": [domain]}
c = requests.post(f"{BASE}/v1/people/search?count=true&limit=1", headers=H, json=body)
if not c.ok or c.json().get("meta", {}).get("total") != 1:
return None, "not one match"
s = requests.post(f"{BASE}/v1/people/search", headers=H, json={**body, "limit": 1})
rows = s.json().get("data", []) if s.ok else []
if not rows:
return None, "no row"
pid = rows[0]["meta"]["source_ids"]["person_id"]
w = requests.get(f"{BASE}/v2/people/email/work", headers=H, params={"person_id": pid})
data = w.json().get("data") if w.ok else None
if not data:
return None, "no checked work email"
return data.get("email"), "ok"
with open("people.csv") as f, open("out.csv", "w", newline="") as g:
w = csv.writer(g)
w.writerow(["first_name", "last_name", "domain", "email", "note"])
for row in csv.DictReader(f):
email, note = find_email(row["first_name"], row["last_name"], row["domain"])
w.writerow([row["first_name"], row["last_name"], row["domain"], email, note])
g.flush()
time.sleep(0.6) # 3 calls per row stays under 5 a secondA row that is not a single match costs nothing, because the count is free. A matched row costs at most 2 records. A 500-row file where every row matches costs at most 1,000, which is the whole Free allowance.
On Free the limit is 5 requests a second, and the script makes up to three calls per row. The 0.6-second pause per row keeps it under that. If you run rows in parallel, honour the Retry-After header on a 429.
Past a few hundred 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. See /pricing.
What you get
The work-email endpoint returns one address in data. This is the response shape from the API reference, with placeholder values.
{
"success": true,
"data": {
"email": "jane.doe@acme.com",
"status": "verified",
"verified_batch_date": "2026-09-20"
}
}verified_batch_date is the run the check came from. Null means the check was not ours. When nothing qualifies, data is null. Your output CSV ends up like this:
first_name,last_name,domain,email,note
Jane,Doe,acme.com,jane.doe@acme.com,ok
John,Roe,acme.com,,not one matchReview every row marked not one match by hand. Those are where the company, the spelling or a common name needs a second column.
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, 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. Paid plans slow down instead of answering 402 |
meta.total is 0 | Name spelled differently, or the domain is not the company's main site | Drop q to a surname, or search the domain alone |
data: null from the work-email call | No address qualifies at the current employer | Try reveal_email on enrich, or skip the row |
| 400 from a bulk export | More than 50,000 rows in one request | Split the export, 500,000 rows a day is the ceiling |
FAQ
Can I upload the CSV and get emails back?
Not as a file upload. The Exports page in the app and POST /v1/exports build a CSV from filters, up to 50,000 rows a request. To match your own list of names, loop the API as in step 5.
What if my CSV has only names?
Add a company or domain column first. A name alone matches many people, and each row you search costs a record. The free count in step 2 shows you which rows are ambiguous.
How do I know the email is safe to send?
The work-email endpoint returns only checked addresses. Read the status. verified and catch_all_valid are the safest, catch_all is a risk you accept. LeadOcean does not refund bounces.
Does it work for personal emails?
Yes, through GET /v2/people/email/personal. Use it when the work address is null and you have a lawful reason to write to a personal one.
Can an agent run the loop for me?
Yes. The hosted MCP server at https://api.leadocean.io/mcp exposes 13 tools, including count_leads, which is free, and search_leads and get_lead, which cost one record per person.
More how-tos live in the blog hub. Related: find email from a name and company, find email from Twitter and find email from an email signature.
Turn your CSV of names into work emails
Free to start. No credit card. 1,000 records to spend whenever you like.
Get your free API key →