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 Name and Company (2026)

From Jane Doe at Acme to a work email you can check before you send. By hand first, then in bulk.

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

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 with reveal_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 verified or catch_all_valid addresses. 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.
  • curl and jq for the single lookup. Python 3 with requests for 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:

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

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": 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.

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

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

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

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

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

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

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, 50 on Solo, 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
Empty data from searchName spelled differently, or the domain is not the company's main siteDrop q to a surname, or search the domain alone and scan the rows
404 from enrichNo person with that id, or no identifier we can enrich byDo not retry. Try another match from the search
503 no_sourceThe lookup could not be reachedRetry 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 →