Pre-Black Friday: 20% off, for as long as you stayEARLYBFon Solo and AgencyEnds Nov 28Start free

How-to guide

How to Find an Email From a CSV of Names (2026)

From a spreadsheet of names to a column of work emails you can check before you send. Manual first, then in bulk.

Get your free API key →Free to start. No credit card. 1,000 records to spend whenever you like.

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_valid or catch_all work addresses, or null.

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_name and company (or domain). 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:

  1. Split names into first_name and last_name. Drop titles such as "Dr." and suffixes such as "Jr.".
  2. Put one person per row and remove duplicates.
  3. Add a company column. A domain like acme.com matches better than "Acme Inc".
  4. Fix spelling and encoding. "Jose" and "José" are different strings to a filter.

A clean file looks like this:

csv
first_name,last_name,company,domain
Jane,Doe,Acme,acme.com

The 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.

bash
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.
  • 2 or more: ambiguous. Add a title or country column, 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.

bash
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.

bash
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.

python
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 second

A 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.

json
{
  "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:

csv
first_name,last_name,domain,email,note
Jane,Doe,acme.com,jane.doe@acme.com,ok
John,Roe,acme.com,,not one match

Review every row marked not one match by hand. Those are where the company, the spelling or a common name needs a second column.

Troubleshooting

ErrorCauseFix
401Missing or wrong x-api-key headerRe-export $LEADOCEAN_API_KEY and send it as x-api-key
429Above the plan's rate limit (5 a second on Free, 200 on Agency)Honour the Retry-After header and slow the loop
402Records spent. On Free the 1,000 are one-offMove to Solo ($97 a month) or Agency ($497 a month), see /pricing. Paid plans slow down instead of answering 402
meta.total is 0Name spelled differently, or the domain is not the company's main siteDrop q to a surname, or search the domain alone
data: null from the work-email callNo address qualifies at the current employerTry reveal_email on enrich, or skip the row
400 from a bulk exportMore than 50,000 rows in one requestSplit 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 →