Home / Blog

How to Extract Emails From a Website List (Plus Phones and Socials)

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 /contact when the starting page has no contact or legal link. German-speaking domains also get an /impressum guess and French domains a /mentions-legales guess.
  • 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 like name [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

  1. 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.
  2. Open the website contact details scraper and paste the list into the urls field.
  3. Set maxItems to cap how many websites the run processes, and maxPagesPerDomain if you want a wider search than 5 pages per site.
  4. Decide on the email filters. onlyGenericEmails keeps role addresses like info@ or sales@ and drops addresses that look like a named person. sameDomainEmailsOnly drops addresses on other domains, such as an agency that built the site.
  5. Start the run. Each website becomes one dataset row.
  6. 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

FieldWhat it contains
domainNormalized website domain, one row per domain
inputUrlThe URL or domain exactly as you supplied it
statusok, no_contacts, blocked, js_only, not_found or error
billedTrue only when the row was charged as a result
emailsCleaned, deduplicated email addresses found on the site
emailTypesEach email labeled generic (info@, sales@) or personal, by naming rules
emailSourcePagesFor each email, the first page where it appeared
phonesPhone numbers in E.164 format
phoneSourcePagesFor each phone number, the first page where it appeared
linkedin, facebook, instagram, twitterProfile URL linked from the site, or null (youtube and tiktok have their own fields)
contactPageUrlThe contact page discovered during the crawl, if any
companyNameOrganization 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. Only ok rows have contacts. blocked and js_only rows are sites you may want to check by hand.
  • Open the source page. emailSourcePages and phoneSourcePages show 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 sameDomainEmailsOnly to drop them automatically.
  • Separate generic and personal addresses. emailTypes lets you route info@ and sales@ addresses one way and named-person addresses another, or drop the personal ones with onlyGenericEmails. 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 foundCost
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.

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 personal such 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 onlyGenericEmails filter 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 scrapedAt timestamp 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.