# Website Lead Extractor: Emails, Phones & Addresses (`f0rty7even/website-lead-extractor`) Actor

Turn a list of company websites into a clean B2B lead list. Extracts emails, phone numbers in E.164, postal addresses, PO boxes, VAT and company registration numbers and social profiles, verifies every email against DNS, and returns one row per company. Pay per company, never per page.

- **URL**: https://apify.com/f0rty7even/website-lead-extractor.md
- **Developed by:** [Michael Yousrie](https://apify.com/f0rty7even) (community)
- **Categories:** Lead generation, Automation
- **Stats:** 1 total users, 1 monthly users, 66.7% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $7.00 / 1,000 leads

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/platform/actors/running/actors-in-store#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Website Lead Extractor: Emails, Phones and Company Addresses

Give it a list of company websites. Get back one clean row per company, ready to paste into a CRM.

Most contact scrapers hand you a pile of page-level rows and leave you to stitch them together. This one returns a **company record**: the best email, the phone number in E.164, the postal address split into street, city and postcode, the VAT and company registration numbers, and the social profiles. Every email is checked against DNS before you see it.

**You pay per company, not per page. If a site publishes nothing, you are not charged.**

***

### What you get for every website

| Field | Example | Notes |
|---|---|---|
| `bestEmail` | `sales@northwind.co` | The single address most likely to reach a human |
| `emails[]` | 6 addresses | Each with department, role flag and deliverability |
| `bestPhone` | `+442079460958` | E.164, ready for a dialler or a CRM |
| `phones[]` | 3 numbers | With national format and line type |
| `street`, `city`, `region`, `postalCode`, `countryCode` | `12 Baker Street`, `London`, `NW1 6XE`, `GB` | Parsed, not a blob of text |
| `addresses[]` | every office found | Each with a confidence level and its source page |
| `poBoxes[]` | `PO Box 4321` | Recognised in 8 languages |
| `vatNumber` | `GB123456789` | Validated against the country's real VAT format |
| `companyNumber` | `08123456`, `HRB 6089` | UK, German, Dutch, French and Nordic registries |
| `linkedin`, `twitter`, `facebook`, `instagram`, `youtube`, `tiktok` | canonical profile URLs | Share buttons and tracking links removed |
| `mxProvider` | `Google Workspace` | Who runs their mail, from the MX records |
| `companyName` | `Northwind Ltd` | From the site's own declared name |
| `found` | `true` | `false` means nothing was published, and nothing was charged |
| `error`, `errorDetail` | `blocked`, plus what to do about it | Plain English, not a stack trace |

Export as JSON, JSONL, CSV or Excel, or pull it straight from the API.

***

### How to use it

Paste domains into **Websites to extract leads from**. Any of these work, and all three of these are recognised as the same company and charged once:

```
northwind.co
https://www.northwind.co/contact
NORTHWIND.CO/
```

You can also paste a whole spreadsheet column separated by commas or new lines. That is it. Everything else has a sensible default.

#### Input options

| Option | Default | What it does |
|---|---|---|
| `startUrls` | required | Websites to enrich. Domains or full URLs |
| `maxPagesPerDomain` | 5 | Pages read per site. Costs you nothing extra, because billing is per company |
| `verifyEmails` | true | MX lookup, disposable detection, mail provider detection |
| `enableBrowserFallback` | true | Retries with a real browser when a site is JavaScript-only or blocks plain requests |
| `maxWebsites` | unlimited | Hard spend cap on large inputs |
| `maxConcurrency` | 10 | Sites processed at once |
| `proxyConfiguration` | datacenter | Switch to Residential for sites behind heavy bot protection |

***

### Why the results are cleaner

Most of the work in this Actor is not finding contact details. It is refusing to return things that only look like contact details.

**Invented emails are the worst defect a lead list can have.** They pass a visual check, they bounce, and the bounces damage your sending domain. Ordinary English is full of phrases like "reach them at northwind.co", and a naive scraper turns that into `them@northwind.co`. This one requires an explicit obfuscation marker, so `sales [at] northwind [dot] co` is decoded and "located at northwind.co" is left alone. Retina image filenames (`logo@2x.png`), Sentry keys and `you@example.com` placeholders are filtered out too.

**Cloudflare-obfuscated addresses are decoded.** Cloudflare's email protection XOR-encodes addresses so that regex scrapers see nothing. It is on by default for a large share of small business sites. Those addresses are decoded and returned.

**Third-party vendor emails are dropped.** Scraping a shop that uses Intercom should not return Intercom's legal address. Around fifty common vendor domains are excluded unless you are scraping that vendor.

**Share buttons are not social profiles.** `facebook.com/sharer`, `twitter.com/intent/tweet` and LinkedIn's share widget are excluded. What you get is the company's actual page.

**A date is not an address.** "June 09, 2025" contains a street-shaped fragment and a four digit number that is a valid postcode in Australia. Dates, years, IBANs, court registrations and cookie notices are all rejected before an address is accepted.

**Every address carries its confidence.** `high` means the site published structured schema.org data and no guessing was involved. `medium` is a semantic `<address>` element. `low` is a scored read of the footer or contact page, and it always includes the untouched original text so you can check it.

***

### Where the contact details come from

The Actor reads the homepage, then picks the few pages that actually carry contact details: contact, about, team, locations, and the legal notice. It does not crawl blog posts or product listings.

Legal notice pages are prioritised almost as highly as contact pages. In Germany, Austria and Switzerland the Impressum is legally required to carry the full postal address, the managing director and the VAT identification number, which makes it the richest page on most European sites.

Five pages is enough for nearly every website. Raising the limit finds a little more on very large sites and costs you nothing extra.

***

### Pricing

**$10 per 1,000 companies enriched.** One charge per company that produced at least one contact.

You are not charged when:

- the domain does not exist
- the site cannot be reached
- the site is reachable but publishes no contact details at all
- the only thing found was a generic Instagram or Facebook link

Compare that to paying per page. A crawler at $1.05 per 1,000 pages reading 20 pages per site works out to about $21 per 1,000 companies, and gives you no addresses, no VAT numbers, no E.164 phones and no email verification.

***

### Common uses

**Building a B2B outreach list.** Export a domain column from Apollo, Crunchbase or a directory, run it through here, and get verified emails with the role and department already labelled so you can route sales enquiries differently from support enquiries.

**Enriching a CRM that has domains but no contacts.** One row per company maps directly onto a company record. `bestEmail` and `bestPhone` are single values, so there is nothing to reduce or deduplicate on import.

**Supplier and counterparty due diligence.** The registered address, VAT number and company registration number are what a KYB or onboarding check actually needs, and they come from the company's own legal notice page.

**Direct mail and field sales.** Postal addresses are parsed into street, city, region, postcode and ISO country code, so they are ready for a mailing house without manual cleanup.

**Cleaning a list you already have.** Run it with `verifyEmails` on and use the `deliverability` field to drop addresses that would bounce before you load them into a sending tool.

**Local business research.** Phone numbers come back in E.164 with the line type, so mobile numbers can be separated from switchboards.

***

### Email verification, described honestly

Verification is done entirely with DNS. For each address the Actor checks the syntax, looks up the domain's MX records, checks a bundled list of over 8,000 disposable email domains, and identifies the mail provider.

| `deliverability` | Meaning |
|---|---|
| `valid` | The domain has MX records and is not disposable. Mail can be delivered to it |
| `risky` | Deliverable but worth a second look: a free mailbox provider, a disposable domain, or a domain with no MX record |
| `invalid` | Malformed, or the domain cannot receive mail at all |
| `unknown` | DNS did not answer. Never silently upgraded to `valid` |

**There is no SMTP probe, deliberately.** Connecting to someone else's mail server to test whether a specific mailbox exists gets shared IPs blocklisted, is rate limited into uselessness at any real volume, and is treated as address harvesting by the receiving side. So this will tell you that a domain can receive mail and that an address is not a throwaway. It will not tell you that one particular mailbox exists. Any tool that claims otherwise at scale is either guessing or getting blocked.

***

### Sites that block scrapers

Some commercial sites answer a plain HTTP request with a 403 and answer a real browser with the page. When that happens the Actor automatically retries in a headless browser, which clears most of them.

For the stubborn ones, set **Proxy configuration** to Residential and re-run just those domains. The `errorDetail` field tells you which rows to retry and why. Blocked rows are never charged.

***

### Output example

```json
{
  "domain": "northwind.co",
  "companyName": "Northwind Ltd",
  "found": true,
  "bestEmail": "sales@northwind.co",
  "bestPhone": "+442079460958",
  "bestAddress": "12 Baker Street, London, NW1 6XE, United Kingdom",
  "street": "12 Baker Street",
  "city": "London",
  "postalCode": "NW1 6XE",
  "countryCode": "GB",
  "vatNumber": "GB123456789",
  "companyNumber": "08123456",
  "mxProvider": "Google Workspace",
  "linkedin": "https://linkedin.com/company/northwind",
  "emails": [
    {
      "email": "sales@northwind.co",
      "department": "sales",
      "isRoleAccount": true,
      "isSameDomain": true,
      "isFreeProvider": false,
      "validation": {
        "deliverability": "valid",
        "reason": "mx_record_present",
        "hasMx": true,
        "mxProvider": "Google Workspace"
      },
      "sourceUrl": "https://northwind.co/contact"
    }
  ],
  "phones": [
    {
      "e164": "+442079460958",
      "national": "020 7946 0958",
      "countryCode": "GB",
      "type": "fixed_line",
      "sourceUrl": "https://northwind.co/contact"
    }
  ],
  "pagesCrawled": ["https://northwind.co/", "https://northwind.co/contact"],
  "error": null
}
```

***

### Frequently asked questions

**How do I extract emails from a list of websites?**
Paste the domains into the input field and run it. One row comes back per company, with every email found on the site, each labelled by department and checked against DNS.

**Can it scrape a company's phone number and address as well as the email?**
Yes. Phone numbers are returned in E.164 with the line type, and postal addresses are split into street, city, region, postcode and ISO country code.

**Does it find the VAT number or company registration number?**
Yes, when the site publishes them, which most European companies must. UK company numbers, German HRB numbers, Dutch KvK numbers, French SIREN and SIRET numbers and EU VAT identifiers are recognised, and each is validated against its country's real format so that an order reference is never returned as a VAT number.

**What happens if a website has no contact details?**
You get a row with `found: false` and empty arrays, and you are not charged for it.

**How many pages does it read per website?**
Five by default, chosen by relevance rather than crawled breadth first. You can raise it to 25. It does not change the price.

**Is this GDPR compliant?**
The Actor reads only what a company publishes on its own public website, which is typically business contact data rather than personal data. How you use the output is your responsibility: if you contact people in the EU or UK, you need a lawful basis and you must honour opt-outs. This tool does not extract named individuals' personal profiles.

**Can I run it on a single website?**
Yes. One domain in, one enriched company record out, for one cent.

**How is this different from a generic contact details scraper?**
Three things: one row per company instead of one per page, postal addresses and registration identifiers that generic scrapers do not return at all, and DNS verification on every email. Plus per-company pricing, which works out at less than half the effective cost of per-page crawling.

***

### Other tools by the same author

- [LLM-Ready Web Extractor](https://apify.com/f0rty7even/llm-web-extractor): any URL to clean Markdown or JSON
- [SEO Audit and Site Health Checker](https://apify.com/f0rty7even/seo-auditor): on-page SEO issues and a health score per page
- [Link Preview and URL Metadata API](https://apify.com/f0rty7even/link-preview): Open Graph and metadata extraction, free
- [AgentReader](https://apify.com/f0rty7even/agent-reader): clean web content for AI agents, over MCP

***

### Notes

Public data only. The Actor reads pages that any visitor can load, and it does not log in, bypass paywalls or collect data from behind authentication.

# Actor input Schema

## `startUrls` (type: `array`):

One or more company websites. Paste bare domains (northwind.co), full URLs (https://northwind.co/contact) or a whole spreadsheet column separated by commas or new lines. Every form of the same domain is merged, so a company is crawled and charged only once.

## `maxPagesPerDomain` (type: `integer`):

How many pages to read on each site. The homepage is always read, then the highest-value contact pages (contact, about, imprint, team, locations). Five is enough for almost every site: raising it finds slightly more on large sites and costs you nothing extra, because you are billed per company, not per page.

## `verifyEmails` (type: `boolean`):

Check every email's domain for MX records, flag disposable and free-mailbox addresses, and detect the mail provider (Google Workspace, Microsoft 365 and so on). Adds a second or two per site and removes the addresses that would bounce.

## `enableBrowserFallback` (type: `boolean`):

If a site loads fine but publishes no contact details in its HTML, open it in a real browser and try again. Only fires on sites that need it. Turn it off for the fastest possible run on plain HTML sites.

## `maxWebsites` (type: `integer`):

Hard stop after this many websites, as a safety net on large inputs. Leave empty to process everything you supplied.

## `maxConcurrency` (type: `integer`):

How many sites to work on at once. Raise it to finish large lists faster; lower it if you are being polite to a set of small servers.

## `proxyConfiguration` (type: `object`):

Proxy used to reach the websites. The default rotating datacenter proxy is included on every plan and works for most sites. If a site behind Cloudflare or a WordPress security plugin blocks it, switch to Residential.

## Actor input object example

```json
{
  "startUrls": [
    "northwind.co",
    "https://contoso.com"
  ],
  "maxPagesPerDomain": 5,
  "verifyEmails": true,
  "enableBrowserFallback": true,
  "maxConcurrency": 10,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

## `leads` (type: `string`):

One row per company, ready for a CRM. Export as JSON, JSONL, CSV or Excel.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "startUrls": [
        "apify.com",
        "https://www.patagonia.com"
    ],
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("f0rty7even/website-lead-extractor").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "startUrls": [
        "apify.com",
        "https://www.patagonia.com",
    ],
    "proxyConfiguration": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("f0rty7even/website-lead-extractor").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "startUrls": [
    "apify.com",
    "https://www.patagonia.com"
  ],
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call f0rty7even/website-lead-extractor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,f0rty7even/website-lead-extractor"
        }
    }
}

```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/cgOwW2jQ5W7ctyrKv/builds/5ichlfal4HLsdf3av/openapi.json
