# Website Contact Finder (Dataset, CSV or Sheet) (`nerolabs/website-contact-finder`) Actor

Finds published business contacts for every website in an Apify dataset, CSV file or Google Sheet. Returns each original row plus role inbox emails, phones in E.164, socials and the contact page. Inputs: datasetId or fileUrl, websiteField. Charged per site with a contact. Agent-ready via x402, MCP.

- **URL**: https://apify.com/nerolabs/website-contact-finder.md
- **Developed by:** [Adam Pearce](https://apify.com/nerolabs) (community)
- **Categories:** Lead generation, Business, Agents
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $14.00 / 1,000 website contacts

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/actors/running/actors-in-store.md#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 Contact Finder (Dataset, CSV or Sheet)

You have a list of companies. What you need is a way to reach them.

Point this Actor at the list you already have, an Apify dataset, a CSV, an Excel file or a Google Sheet, and it opens each company's website, finds the contact details they have published, and hands back **every original column untouched** with the contact columns added alongside. No copying a domain column out, no matching results back to company names by hand.

### What it returns, and what it deliberately does not

By default this Actor returns **role inboxes only**: shared business addresses like `info@`, `sales@`, `enquiries@` and `bookings@`, which identify a function rather than a named person.

Addresses that look like one individual's own (`firstname.lastname@`), no-reply inboxes, and anything the classifier is not confident about are **counted but not returned**. You can see exactly how many were withheld and why, on every row. You can switch personal addresses on, and the input field tells you what comes with that decision.

This is a deliberate design choice, not a limitation. Under UK and EU rules a shared business inbox that identifies no individual is treated very differently from a named person's address, and the second one brings real obligations for whoever collects it. Defaulting to role inboxes keeps the output useful for B2B outreach while leaving the sharper edges opt-in.

Two other things this Actor will not do: it never **guesses or generates** an address (no inventing `firstname.lastname@domain` and hoping), and it never returns a number that is not a real phone number.

### What makes it different from the other contact scrapers

**Every other contact finder on the Store takes a typed-in list of domains.** If your companies live in a scraped dataset, a CRM export or a spreadsheet, you have to pull the domain column out, paste it in, and then reattach the results to your own rows. This one takes the whole thing.

The other difference is what it throws away. A real business website is full of email addresses that are not the business's:

| What a naive scraper returns | What this one does |
|---|---|
| `support@webador.com` | Dropped. That is the website builder the company used, not the company. |
| `0689...fa@o23827.ingest.sentry.io` | Dropped. An error-tracking key that happens to contain an `@`. |
| `yourname@example.com` | Dropped. A placeholder in the page template. |
| `u003ehelp@company.com` | Dropped. A fragment of escaped JSON, not an address. |
| `logo@2x.png` | Dropped. An image filename. |
| `tel:000000000027434378` | Dropped. An advertising tracking id sitting in a phone link. |

Every example in that table came off a real website during testing, not from imagination. The single most effective filter is simple: **keep addresses at the company's own domain**, which is on by default and removes most of the noise in one move. Off-domain addresses are still counted and can be switched back on.

### What you get on every row

| Column | What it is |
|---|---|
| `primaryEmail` | The best role inbox found, ready to use |
| `contactEmails` | All returned addresses for that site |
| `emailClasses` | Per address: its class, whether it is at the site's own domain, and whether it came from a `mailto:` link or body text |
| `excludedEmailCounts` | What was found but withheld, and why |
| `primaryPhone` / `contactPhones` | Published numbers, normalised to E.164 with Google's libphonenumber |
| `socialProfiles` | The company's own LinkedIn, X, Facebook, Instagram, YouTube and TikTok links |
| `contactPageUrl` | The contact page it found, useful when there is no address to publish |
| `contactStatus` | `ok`, `no_contacts_found`, `blocked`, `unreachable`, `disallowed_by_robots` or `invalid_url` |
| `websiteDomain` | The registrable domain, handy for grouping and deduplication |

### How it crawls

The homepage first, then the most contact-like pages on the same site, up to a page budget you control (four by default).

That matters more than it sounds. In testing, restricting the run to homepages only found **nothing at all** on sites that gave up their address readily once the contact page was followed. Most businesses do not put an email on the front page.

It reads **robots.txt and honours it** by default. Most contact pages are allowed, so this rarely costs a result, and it means the run does what site owners have actually asked automated visitors to do. You can switch it off for your own crawls.

### Pricing

- **$0.02** per website that returns a contact
- **$0.002** per website reached that publishes nothing (a real answer, at a tenth of the price)
- **Nothing at all** for a site that could not be reached, blocked the request, or was disallowed by its own robots.txt

So a 1,000-company list where 6 in 10 publish something costs about **$13**. A hundred companies is about **$1.30**. Adding a CSV and an Excel export is two cents.

Store discount tiers are on from day one, so Bronze, Silver and Gold plan customers pay 10, 20 and 30 percent less.

### FAQ

**What hit rate should I expect?**
It depends entirely on your list. Small and mid-size businesses, trades, agencies, clinics and local firms usually publish an address or a phone number, and those lists do well. Large consumer brands frequently publish neither, on purpose, and route everything through a contact form instead, so a list of household names will look disappointing no matter which tool you use. The `contactStatus` column tells you which case each row was, so you are never guessing.

**Some sites came back `blocked`. Why?**
A minority of sites refuse automated requests outright. The Actor uses Apify's residential proxy by default, which clears most of it, and reports the rest honestly as `blocked` rather than pretending the company has no contact details. Blocked sites are not charged.

**Can it find a specific person's email at a company?**
No, and it will not pretend to. It returns what a company has chosen to publish. Tools that produce a named individual's address usually guess it from a name-and-domain pattern, and this Actor deliberately does not do that.

**Is this legal to use?**
Reading a page a company has published is not the hard part; what you then do with the details is. You are the controller of whatever you collect here. In the UK, marketing email to a limited company is treated differently from marketing email to a sole trader, and no tool can tell you from a website which of the two you are looking at. Keeping the default (role inboxes only) puts you in a far simpler position than harvesting named individuals. This is not legal advice, and if you are running outreach at scale it is worth getting your own.

**Does it store anything?**
No. The results go to your own dataset in your own account. Nothing is kept, and the optional webhook payload carries counts and download links only, never the contact details themselves.

**Can an AI agent run this?**
Yes. Pay per event, limited permissions, no standby mode, so it is payable through x402 and callable over MCP.

### The rest of the toolkit

Natural next steps once you have the contacts: **[Email List Cleaner & Validator](https://apify.com/nerolabs/email-list-cleaner)** checks the addresses are real and drops throwaway domains and duplicates, and **[Phone Number Validator & Cleaner](https://apify.com/nerolabs/phone-number-validator)** does the same for the numbers, telling you which are mobiles.

For the data itself: [Dataset Cleaner & Exporter](https://apify.com/nerolabs/dataset-cleaner-exporter), [Filter & Transform](https://apify.com/nerolabs/dataset-filter-transform), [Join & Merge](https://apify.com/nerolabs/dataset-join-merge), [Aggregate, Group By & Pivot](https://apify.com/nerolabs/dataset-aggregate-pivot), [Diff & Change Detector](https://apify.com/nerolabs/dataset-diff-detector), [AI Enrich](https://apify.com/nerolabs/dataset-ai-enrich), [Charts & Report](https://apify.com/nerolabs/dataset-charts-report), [to Postgres, Supabase & MySQL](https://apify.com/nerolabs/dataset-to-database), [to REST API](https://apify.com/nerolabs/dataset-to-rest-api), and [Actor Pipeline Runner](https://apify.com/nerolabs/actor-pipeline-runner) to chain them in one call.

***

If this saved you an afternoon of opening contact pages by hand, a review on the Store page helps a lot. If something looks wrong, open an issue on the Issues tab and I will answer personally.

# Actor input Schema

## `datasetId` (type: `string`):

Pick an existing Apify dataset. Use this OR 'File URL' OR 'Data (inline)'. Declaring it this way is what lets this Actor run with limited permissions: it may read the dataset you point at, and nothing else on your account.

## `fileUrl` (type: `string`):

A public link to a CSV or TSV file, an Excel .xlsx file, a JSON array or JSON Lines file, or a Google Sheet (paste the normal sheet link with sharing set to 'Anyone with the link can view'). Format is detected automatically. Up to 100 MB.

## `data` (type: `array`):

Paste rows directly instead, for a quick one-off check.

## `fileFormat` (type: `string`):

Only needed if automatic detection gets the file URL's format wrong.

## `websiteField` (type: `string`):

Which column holds the website or domain, for example 'website' or 'url'. Leave empty and it is detected automatically, first by column name and then by looking at which column actually contains domains. A bare domain like 'acme.co.uk' works as well as a full URL.

## `includePersonalEmails` (type: `boolean`):

OFF by default, deliberately. When off you get role inboxes only (info@, sales@, enquiries@ and similar), which identify a function rather than a person. Turning this on also returns addresses like firstname.lastname@, which are personal data under UK and EU rules and bring obligations with them: you become responsible for telling those people you hold their data, and for honouring objections. Leave it off unless you have a reason and a basis.

## `includeNoReply` (type: `boolean`):

OFF by default. noreply@ and do-not-reply@ are real addresses that nobody reads, so they are withheld unless you actually want them.

## `onlyOwnDomain` (type: `boolean`):

ON by default, and it is the single biggest quality filter. A business website is full of other people's addresses: the website builder's support inbox, an analytics vendor, a partner. Keeping only addresses at the site's own registrable domain removes almost all of that. Turn it off to also see off-domain addresses, which are always reported separately either way.

## `extractPhones` (type: `boolean`):

Reads published tel: links and normalises them to E.164 using Google's libphonenumber, so they are ready for a dialler. A number that cannot be parsed is returned exactly as published rather than guessed at.

## `extractSocials` (type: `boolean`):

Collects the company's own LinkedIn, X, Facebook, Instagram, YouTube and TikTok profile links. Share buttons are ignored.

## `phoneRegion` (type: `string`):

The country to assume for phone numbers published without a country code, such as GB or US. Numbers that already start with + are unaffected.

## `followContactPages` (type: `boolean`):

After the homepage, follow same-site links that look like a contact, about or imprint page, most contact-like first.

## `maxPagesPerSite` (type: `integer`):

Including the homepage. Four is enough for almost every business site and keeps the run polite and quick.

## `respectRobotsTxt` (type: `boolean`):

ON by default. Skips any page the site's own robots.txt asks automated clients not to fetch. Most contact pages are allowed, so this rarely costs you a result, and it means the run does what site owners have asked for.

## `useProxy` (type: `boolean`):

ON by default. Some sites refuse plain datacentre traffic outright, so a residential exit meaningfully raises the hit rate. Turn it off to use the platform's direct connection.

## `concurrency` (type: `integer`):

How many sites to work through in parallel. Each site is still fetched one page at a time.

## `requestTimeoutSecs` (type: `integer`):

How long to wait for a single page before giving up on it.

## `keep` (type: `string`):

Filters the output. Charging is unaffected: it follows what was actually found, not what you keep.

## `exportFormats` (type: `array`):

Optionally write the results as a real downloadable CSV and/or Excel file, on top of the dataset.

## `outputDatasetName` (type: `string`):

Optional. Every run's results are also appended to a dataset of this name, so a scheduled job builds one growing list. Not charged again.

## `maxItems` (type: `integer`):

Stop after this many rows, a cost guard while you are testing. There is a hard ceiling of 50,000 per run regardless.

## `webhookUrl` (type: `string`):

Optional. If set, the run summary (counts and download links, never the contact details themselves) is POSTed here as JSON when the run finishes. Only charged on a confirmed 2xx.

## Actor input object example

```json
{
  "data": [
    {
      "company": "Apify",
      "website": "apify.com"
    },
    {
      "company": "Electronic Frontier Foundation",
      "website": "eff.org"
    },
    {
      "company": "Free Software Foundation",
      "website": "https://www.fsf.org/"
    },
    {
      "company": "Mozilla",
      "website": "mozilla.org"
    }
  ],
  "fileFormat": "auto",
  "includePersonalEmails": false,
  "includeNoReply": false,
  "onlyOwnDomain": true,
  "extractPhones": true,
  "extractSocials": true,
  "phoneRegion": "GB",
  "followContactPages": true,
  "maxPagesPerSite": 4,
  "respectRobotsTxt": true,
  "useProxy": true,
  "concurrency": 5,
  "requestTimeoutSecs": 20,
  "keep": "all",
  "exportFormats": []
}
```

# Actor output Schema

## `results` (type: `string`):

Every original row with the business contact details found for its website added.

## `contactSummary` (type: `string`):

Hit rates, statuses, what was withheld and why, plus the data-protection note.

# 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 = {
    "data": [
        {
            "company": "Apify",
            "website": "apify.com"
        },
        {
            "company": "Electronic Frontier Foundation",
            "website": "eff.org"
        },
        {
            "company": "Free Software Foundation",
            "website": "https://www.fsf.org/"
        },
        {
            "company": "Mozilla",
            "website": "mozilla.org"
        }
    ],
    "phoneRegion": "GB"
};

// Run the Actor and wait for it to finish
const run = await client.actor("nerolabs/website-contact-finder").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 = {
    "data": [
        {
            "company": "Apify",
            "website": "apify.com",
        },
        {
            "company": "Electronic Frontier Foundation",
            "website": "eff.org",
        },
        {
            "company": "Free Software Foundation",
            "website": "https://www.fsf.org/",
        },
        {
            "company": "Mozilla",
            "website": "mozilla.org",
        },
    ],
    "phoneRegion": "GB",
}

# Run the Actor and wait for it to finish
run = client.actor("nerolabs/website-contact-finder").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 '{
  "data": [
    {
      "company": "Apify",
      "website": "apify.com"
    },
    {
      "company": "Electronic Frontier Foundation",
      "website": "eff.org"
    },
    {
      "company": "Free Software Foundation",
      "website": "https://www.fsf.org/"
    },
    {
      "company": "Mozilla",
      "website": "mozilla.org"
    }
  ],
  "phoneRegion": "GB"
}' |
apify call nerolabs/website-contact-finder --silent --output-dataset

```

## MCP server setup

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

```

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/wnlF2r2SUccS28jY3/builds/TqfeRjhad0Pwq8JHH/openapi.json
