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

How-to guide

How to Find Email Addresses on a Company Website (Script and API)

A site shows you role inboxes and a domain. Here is the script that collects them, and the lookup that gets you to a person.

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

A company website gives you role inboxes and a domain, rarely a person's address. Collect what the site publishes with a short script, then use the domain to look up the person you want.

TL;DR

  • A company site mostly shows role inboxes such as info@ and sales@. The useful things on it are the domain and the names on the team page.
  • It takes about 10 minutes for one site. The scrape costs nothing. The API step spends about 4 LeadOcean records per person: 3 for a search at limit 3, 1 for the work-email lookup.
  • Collect addresses with a short script, then search the domain for the person you want and read their email_status before you send.

Prerequisites

  • A LeadOcean account from app.leadocean.io. The free plan has no card and gives 1,000 records, one-off.
  • Python 3 (the script uses only the standard library), curl and jq.
  • An API key in your shell as $LEADOCEAN_API_KEY. Free keys get both scopes, search and enrich.
bash
export LEADOCEAN_API_KEY="paste-your-key-here"

Step 1: Check the likely pages by hand

Open the contact, team, about, press and careers pages, then the footer. Search the page source (Cmd+Option+U or Ctrl+U) for mailto: and for @acme.com.

That catches addresses hidden behind an "Email us" button. If the source is nearly empty, the page is rendered by JavaScript. Search the live DOM in DevTools instead.

text
site:acme.com "@acme.com"

The search operator above asks a search engine for indexed pages that mention the domain's addresses, including old PDFs and blog posts. For five sites, stop here. For more, script it.

Step 2: Script the collection

This script fetches a short list of paths, pulls addresses from mailto: links and visible text, and keeps only the site's own domain. It checks robots.txt first and flags role inboxes.

python
import re, sys, time, urllib.request, urllib.robotparser
from urllib.parse import urljoin, urlparse

PATHS = ["/", "/contact", "/about", "/team", "/press", "/careers"]
ROLES = {"info", "sales", "hello", "contact", "support", "press", "jobs", "careers", "privacy", "legal"}
EMAIL = re.compile(r"[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}")
UA = "site-email-audit/1.0"

def fetch(url):
    req = urllib.request.Request(url, headers={"User-Agent": UA})
    with urllib.request.urlopen(req, timeout=10) as r:
        return r.read().decode("utf-8", "ignore")

base = sys.argv[1].rstrip("/")            # https://acme.com
domain = urlparse(base).netloc.removeprefix("www.")
rp = urllib.robotparser.RobotFileParser(base + "/robots.txt")
rp.read()

found = set()
for path in PATHS:
    url = urljoin(base + "/", path.lstrip("/"))
    if not rp.can_fetch(UA, url):
        continue
    try:
        html = fetch(url)
    except Exception as e:
        print("skip", url, e, file=sys.stderr)
        continue
    found |= {m.lower() for m in EMAIL.findall(html) if m.lower().endswith("@" + domain)}
    time.sleep(1)

for addr in sorted(found):
    kind = "role" if addr.split("@")[0] in ROLES else "named"
    print(addr, kind)
bash
python3 find_emails.py https://acme.com

Expect a short list, mostly role. Sites hide named addresses behind forms, images or scripts on purpose. Respect robots.txt and the site's terms, and keep the delay.

Step 3: Carry the domain and the names forward

The page gave you two things worth keeping: the domain, and the names and titles on the team page. A role inbox is shared and rarely read by the person you want.

Size the people we hold at that domain for free. A search with count=true as a query parameter and limit at 1 spends no records.

bash
curl -s "https://api.leadocean.io/v1/people/search?count=true" \
  -H "x-api-key: $LEADOCEAN_API_KEY" -H "content-type: application/json" \
  -d '{
    "domain": ["acme.com"],
    "emailStatus": ["verified", "catch_all_valid", "catch_all"],
    "limit": 1
  }' | jq '.meta | {total, totalIsExact, credits}'

Step 4: Search the domain for the person

An address belongs to a person, so search the domain with a title from the team page. Keep limit small. Each person returned costs one record.

bash
curl -s "https://api.leadocean.io/v1/people/search" \
  -H "x-api-key: $LEADOCEAN_API_KEY" -H "content-type: application/json" \
  -d '{
    "domain": ["acme.com"],
    "title": ["Head of Sales", "VP Sales"],
    "limit": 3
  }' | jq '{count: .meta.count, people: [.data[] | {name: .profile_data.profile_full_name, person_id: .meta.source_ids.person_id}]}'

A search row is thin. It carries no address, but it has a person_id at meta.source_ids.person_id. To list a whole company, use GET /v1/companies/acme.com/people. It pages with a cursor and stops at 10,000 rows.

Step 5: Get the work email

Send the person_id to the work-email endpoint. It returns one address or null, and only an address at the current employer that is verified, catch_all_valid or catch_all.

bash
PERSON_ID="paste-person_id-from-step-4"

curl -s "https://api.leadocean.io/v2/people/email/work?person_id=$PERSON_ID" \
  -H "x-api-key: $LEADOCEAN_API_KEY" | jq '.data'

This costs one record per call, including a call that finds nothing. For a whole list of domains, use POST /v1/exports, which needs a paid plan. See how to find an email from a company domain.

What you get

The Step 3 count is a real run. We ran it on 2026-10-21 with leadocean_count_leads, which spends 0 records, for the placeholder domain acme.com.

json
{
  "total": 32,
  "totalIsExact": true,
  "totalAll": 707,
  "distinctCompanies": 2,
  "credits": 0,
  "unsupportedFilters": []
}

That is 32 people whose email status is verified, catch_all_valid or catch_all, the count tool's default. With no email condition the same filter matches 707 (totalAll). Say which one you used. A distinctCompanies of 2 means the matched people span two employer records, so check the company on each row.

The work-email lookup returns this shape. It 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"
  }
}

LeadOcean gives no refund or credit-back for bounced emails, so read status before you send.

Troubleshooting

ErrorCauseFix
401Missing, wrong or revoked keyRe-copy the key from app.leadocean.io and export $LEADOCEAN_API_KEY again.
429Above your plan's rate: 5 requests a second on Free, 50 on Solo, 200 on Agency, per accountWait the Retry-After seconds, then slow the loop.
402The 1,000 free records are spentCheck GET /v1/account, or move to Solo ($97 a month) or Agency ($497 a month) on pricing.
Search returns no rowsA filter value outside the catalogue, such as a job level of "VP Sales"Check exact strings with GET /v1/enums. Job level is VP.
data: null on the work emailNo address at the current employer is verified, catch_all_valid or catch_allCosts one record. Try POST /v1/people/enrich with reveal_email, or pick another person.
Script prints nothingThe site renders in JavaScript, blocks bots or disallows the paths in robots.txtCheck the pages by hand. Do not work around a block.

FAQ

Is it legal to scrape emails from a website?

Ask your lawyer. We do not give legal advice. Read the site's terms and robots.txt, and check the rules for the country you send to before you email anyone.

Why do I only find info@ addresses?

Companies publish addresses for functions, not people. Named addresses are rare on public pages. Use the domain and team-page names to look the person up instead.

How many records does it cost?

The scrape and the count cost nothing. A search at limit 3 plus one work-email call spends about 4 records.

Can I find an email from a name and a company?

Yes. There is no name-plus-domain lookup, so search the domain first, then look up by person_id. Read how to find an email from name and company.

Where do I get a list of domains to start from?

A startup directory works. See how to find emails from a YC company list. More recipes are in the blog hub.

Turn any company website into a checked work email, free

Free to start. No credit card. 1,000 records to spend whenever you like.

Get your free API key →