To extract emails from a website, open its home, contact, imprint and about pages and copy the addresses published there. For one site, do it by hand. For a list of hundreds of company websites, run a contact details scraper that visits those pages for you and returns one row per website with emails, phone numbers, social links and the exact page each email and phone number came from, so you can check them before you send anything.
Is there an official API to get emails from a website?
No. A website is not a database with an email endpoint. Companies publish their contact details as ordinary page content: a mailto: link in the footer, a phone number on the contact page, a legal notice with the company name, links to their LinkedIn or Instagram profiles. There is no public API you can call with a domain and get those details back that we could confirm.
What exists are tools that read those pages for you. The tool described here does not guess addresses: it only returns what the website publishes.
Your three options
1. By hand (free, slow)
Open the site, check the footer, the contact page and the imprint or about page. For five or ten companies this is fine. A list of 500 domains means thousands of page visits and copy-paste typos.
2. A DIY script (free, fragile)
A few lines of Python can download a home page and pull anything that looks like an email:
import re
import requests
html = requests.get("https://example.com", timeout=15).text
emails = set(re.findall(r"[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}", html))
print(emails)This breaks quickly on real sites. It reads one page, so it misses the contact page, and it picks up junk such as image file names and placeholder addresses. It misses addresses written as name [at] company [dot] com or hidden by Cloudflare's email obfuscation. It does not find phone numbers or social profiles, and it cannot tell a bot challenge page from a real page. Fixing all of that is a project of its own.
3. A hosted tool (paid per result)
A hosted scraper does the crawling, cleaning and formatting, and you get a spreadsheet or JSON back. The Contact Details Scraper for Websites is one of these. It runs on Apify, takes a list of URLs or bare domains, and charges only for websites where it actually found a contact.
What the scraper reads
For each website on your list, the scraper:
- Reads the page you supplied, then ranks the same-site links and visits contact and legal pages first, then about and privacy pages.
- Tries conventional paths such as
/contactwhen the starting page has no contact or legal link. German-speaking domains also get an/impressumguess and French domains a/mentions-legalesguess. - Reads up to 5 pages per website by default. You can set this from 1 to 20.
- Extracts emails from
mailto:links, visible text, Cloudflare-obfuscated addresses and masked forms likename [at] company [dot] com, then removes placeholders, documentation examples and image-like strings. - Extracts phone numbers from
tel:links, structured data and page text, validates them with phone-number parsing rules and returns them in E.164 format, such as+17195425631. - Collects LinkedIn, Facebook, Instagram, X/Twitter, YouTube and TikTok profile links, skipping share buttons and embedded posts.
Duplicate domains are skipped, so example.com and www.example.com produce one row, not two.
Step by step: extract emails from a list of websites
- Put your company websites in a list. Full URLs (
https://example.com/contact) and bare domains (example.com) both work. Entries without a scheme are treated as HTTPS. - Open the website contact details scraper and paste the list into the
urlsfield. - Set
maxItemsto cap how many websites the run processes, andmaxPagesPerDomainif you want a wider search than 5 pages per site. - Decide on the email filters.
onlyGenericEmailskeeps role addresses likeinfo@orsales@and drops addresses that look like a named person.sameDomainEmailsOnlydrops addresses on other domains, such as an agency that built the site. - Start the run. Each website becomes one dataset row.
- Export the dataset as JSON, CSV or Excel, or read it through the API.
Run it from Python
Install the client with pip install apify-client, create an API token as described in the Apify API documentation, and store it in the APIFY_TOKEN environment variable:
import os
from apify_client import ApifyClient
client = ApifyClient(os.environ["APIFY_TOKEN"])
run = client.actor("kazkn/website-contact-details-scraper").call(
run_input={
"urls": [
"https://apify.com",
"https://buffer.com",
"https://basecamp.com",
"https://abcplumbing.com",
],
"maxItems": 10,
"maxPagesPerDomain": 5,
"onlyGenericEmails": False,
}
)
for row in client.dataset(run["defaultDatasetId"]).iterate_items():
print(row["domain"], row["status"], row["emails"], row["phones"])The run's key-value store also holds a RUN_SUMMARY record with counts by status, errors grouped by type, pages read and invalid inputs.
What you get back: the output fields
| Field | What it contains |
|---|---|
domain | Normalized website domain, one row per domain |
inputUrl | The URL or domain exactly as you supplied it |
status | ok, no_contacts, blocked, js_only, not_found or error |
billed | True only when the row was charged as a result |
emails | Cleaned, deduplicated email addresses found on the site |
emailTypes | Each email labeled generic (info@, sales@) or personal, by naming rules |
emailSourcePages | For each email, the first page where it appeared |
phones | Phone numbers in E.164 format |
phoneSourcePages | For each phone number, the first page where it appeared |
linkedin, facebook, instagram, twitter | Profile URL linked from the site, or null (youtube and tiktok have their own fields) |
contactPageUrl | The contact page discovered during the crawl, if any |
companyName | Organization or site name when the pages provide it |
Here is a real row from a run on abcplumbing.com, trimmed to the main fields:
{
"domain": "abcplumbing.com",
"status": "ok",
"billed": true,
"emails": ["[email protected]", "[email protected]"],
"emailTypes": {
"[email protected]": "personal",
"[email protected]": "personal"
},
"phones": ["+17195425631"],
"phoneSourcePages": {"+17195425631": "https://abcplumbing.com/"},
"companyName": "ABC Plumbing Pueblo & Colorado Springs Colorado"
}Check the contacts before you send
Before anything goes into a sequence, check the list:
- Filter on
status. Onlyokrows have contacts.blockedandjs_onlyrows are sites you may want to check by hand. - Open the source page.
emailSourcePagesandphoneSourcePagesshow where each value was found. If a phone number sits on a partner's page or a branch office page, you will see it there. - Look at foreign domains. An address on another domain may belong to the web agency, a reseller or a service provider. Turn on
sameDomainEmailsOnlyto drop them automatically. - Separate generic and personal addresses.
emailTypeslets you routeinfo@andsales@addresses one way and named-person addresses another, or drop the personal ones withonlyGenericEmails. The labels come from naming rules, so edge cases can be mislabeled. - Verify deliverability separately. The scraper does not run mailbox checks. A cleaned address is not proof that the mailbox is active or monitored. Likewise, E.164 formatting confirms that a phone number follows numbering rules, not that someone answers it.
What a run costs
The price is $0.60 per 1,000 results, or $0.0006 per website. One result is one website row with status: "ok" and at least one email, phone number or supported social profile. Several emails and phones from the same website still count as one result.
| Websites with contacts found | Cost |
|---|---|
| 100 | $0.06 |
| 1,000 | $0.60 |
| 10,000 | $6.00 |
Rows with no_contacts, blocked, js_only, not_found or error stay in the dataset with billed: false and cost nothing as results. If you submit 1,000 domains and 700 return contacts, you pay for 700. You can also set a maximum cost per run on Apify, and the scraper stops cleanly when that budget is reached. An optional proxy fallback for blocked pages has its own per-page charge when it is available and enabled; it is off by default.
Is it legal?
This is not legal advice, but here are the points that matter.
- Public is not the same as free to use. The scraper only reads pages anyone can open, with no login. That data can still be personal data, especially addresses labeled
personalsuch as[email protected]. - GDPR and similar laws. If you contact people in the EU or the UK, you need a lawful basis for processing their data, you must tell them where you got it, and you must honor objection and deletion requests. Marketing email rules in many countries add consent requirements on top. Generic role addresses carry less risk than named-person addresses, which is one reason the
onlyGenericEmailsfilter exists. - Website terms. Some sites restrict automated access in their terms of use. Read them for the sites that matter to you.
- Your responsibility. The tool does not certify compliance. Keep the source page and the
scrapedAttimestamp of each contact so you can show where and when you collected it.
FAQ
How do I find the contact email of a website?
Check the footer, the contact page and the imprint or legal notice page, where companies most often publish a general address. For many sites at once, a scraper that prioritizes those pages and records the source of each email gets the same result in bulk.
Can I extract emails from a website for free?
Yes, by hand or with your own script. A hosted tool saves the crawling and cleaning work; with this one you pay $0.60 per 1,000 websites where contacts are found and nothing for the rest.
Does it find every email on a website?
No. It reads a bounded number of pages per site (5 by default, up to 20). Addresses inside images or PDFs, behind a login, on unvisited pages or rendered only by JavaScript can be missed.
Does it guess or verify email addresses?
Neither. It does not build addresses from employee names and does not run mailbox or deliverability checks. It returns addresses published on the pages it read, each tied to its source page.
Does it scrape LinkedIn or Instagram profiles?
No. It collects the links to those profiles from the company's own website. It does not open the social networks themselves.


