# Website Email Finder - Fill Missing Company Emails (`tidytools/website-email-finder-crm`) Actor

Fill missing Email cells in a company list that already has Website and Email columns. Read public company websites, keep source row IDs, and charge only sites where an email is found. No email verification or person lookup.

- **URL**: https://apify.com/tidytools/website-email-finder-crm.md
- **Developed by:** [Yukai Lin](https://apify.com/tidytools) (community)
- **Categories:** Lead generation, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.60 / 1,000 processed websites

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?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
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.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## 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.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Website Email Finder — Fill Missing Company Emails

### What does Website Email Finder do?

Fill empty **Email** cells in a company list that already has **Website** and **Email** columns. This focused form uses the existing Website Email Extractor engine to read the public company websites you provide. It returns one Dataset row per source row by default, so you can match results back to your list. It does not find a person's email from a name or LinkedIn profile, and it does not verify deliverability.

### How to use

1. Paste rows with `Website` and `Email` fields into **Company rows with Website and Email columns**. The first-run form has one public example row; replace it with your own company list.
2. Keep **Only fill missing Email cells** on. A row whose Email field already contains an address is skipped without reading its website or charging you. **Email only** billing, one-row-per-source-row output, and robots.txt respect are the other defaults.
3. Run the Actor. The **Company email fill overview** view shows `input`, `businessName`, `domain`, `bestEmail`, `emailStatus`, `charged`, `errorType`, and `error`. Download the **full JSON** for `rowId` and `fill`: `fill` suggests values only for empty source columns, so existing Email values are not overwritten.

### Input example

```json
{
  "rows": [
    {"row_number": 1, "Website": "hofbraeuhaus.de", "Email": ""},
    {"row_number": 2, "Website": "hofbraeuhaus.de", "Email": "existing@example.org"}
  ],
  "onlyMissingEmail": true,
  "requiredContact": "email",
  "outputMode": "sites",
  "findWebsitesForNames": false,
  "respectRobotsTxt": true,
  "roleEmailsOnly": false
}
```

#### Other inputs

You may instead use `datasetId` for an existing company dataset or `domains` for a plain website list. **Only fill missing Email cells** skips filled Email fields in `rows` or dataset records; a plain domain list has no Email column and its sites are still processed. The advanced JSON interface retains the engine's other inputs, including `startUrls`, name lookup, role-only email output, and other output modes. Those settings follow the engine's behavior and the same per-site price.

### Output example

Illustrative values, assuming a listed public company site publishes `info@one-shop.com`:

| input | domain | bestEmail | emailStatus | charged | errorType |
| --- | --- | --- | --- | --- | --- |
| one.localhost | one.localhost | info@one-shop.com | new_email | true | |
| one.localhost | | | already_had_email | false | |
| not a website | | | not_read | false | invalid_input |

The full JSON has the source row identifier and, for the first row, a suggested `fill.Email`. The already-filled row has `skipped: "has_email"` and no website read. A duplicate or skipped source row is not charged merely because another row names the same site. The overview and CSV view show only their declared columns; get `rowId` and `fill` from full JSON.

### Pricing

The existing `site-processed` event and engine price apply: **$0.002 per qualifying website on Free/Bronze**, $0.0018 on Silver, and $0.0016 on Gold and above. With the default `requiredContact: "email"`, a site is charged only when an email is found. A site with only a phone or social profile, a source row that already has Email, an invalid name or URL, a disallowed home page, or a login page is free (`charged: false`). Multiple source rows for the same company site follow the engine's deduplication rules; there is no charge for every row. Set a maximum run cost in Apify before a large list.

### Limitations

- Only public pages on company websites you supply are read. Requests identify as TidyTools. The default follows robots.txt; a home page disallowed for the crawler returns a free `robots_disallowed` error. The Actor does not log in, use private sessions, solve CAPTCHAs, bypass blocks, or use stealth techniques.
- It does not scrape LinkedIn profiles, build lists around individual people, guess addresses, or check whether an email can receive messages. `roleEmailsOnly: false` means published addresses that look like a person's name may appear; use the results responsibly.
- A company name without a website is a free `invalid_input` row with the default `findWebsitesForNames: false`. Advanced JSON can override this and other defaults; results then follow the engine's rules.
- Follow applicable data-protection and anti-spam laws, honor opt-out requests, and respect website terms. Keep only the contact data you need.

### FAQ

**Will an existing Email value be replaced?** No. With the default `onlyMissingEmail: true`, source rows with an email are skipped. The full JSON `fill` object offers values only for empty columns.

**Can I paste just domains?** Yes. `domains` remains available, but those entries have no existing Email column to test, so the Actor processes them.

**Is each source row charged?** No. Billing uses the engine's per-website `site-processed` event and deduplication. A filled-email row is free; a site without a found email is free in the default mode.

# Actor input Schema

## `rows` (type: `array`):

Paste company rows as JSON objects with existing Website and Email columns, e.g. \[{"row_number":1,"Website":"hofbraeuhaus.de","Email":""}]. Each source row gets an output row. Only rows/dataset items with a filled Email field are skipped when Only missing emails is on; use rowId and fill from full JSON to update empty cells.

## `onlyMissingEmail` (type: `boolean`):

On (default): rows or dataset items that already have an email are skipped without reading the site or charging. A plain domain list has no Email column, so its sites are processed. Off: the engine processes all source rows.

## `requiredContact` (type: `string`):

Email only (default): charge a website only when an email is found; a site with only a phone or social profile is returned free. Any contact also counts a phone or social profile. The per-site price is unchanged.

## `domains` (type: `array`):

Optional plain list of public company websites or domains, one per line. This input has no Email column: onlyMissingEmail cannot skip a site based on a pre-existing address. Up to 50,000 per run. Invalid lines are free.

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

Read an existing dataset of company rows with a website field and, optionally, an Email field. Only rows with a filled Email field are skipped when Only fill missing Email cells is on. The Actor reads their public websites, not Google Maps itself.

## `emailField` (type: `string`):

Field with the email you already have (Email, emails, primaryEmail... are found automatically). Used by "Only rows without an email" and for "fill": the found email is only offered for rows where this field is empty, so your own values are never overwritten.

## `rowIdField` (type: `string`):

Field that identifies each row of the dataset or "rows" (returned as rowId). Empty = the first of rowId, row_number, placeId, id; otherwise the row position. Branches of a chain that share one website are read once but keep their own rowId.

## `outputMode` (type: `string`):

Sites (default): one output row per source row, with the best email and row status. Email rows: one row per found address. Both use the same per-website billing; advanced JSON may select either mode.

## `findWebsitesForNames` (type: `boolean`):

Off (default): a company name without a website is a free invalid_input row. Advanced JSON can turn on the engine’s verified website lookup for names; provide a city, address or phone to disambiguate.

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

On (default): skip pages robots.txt disallows for TidyToolsContacts. A disallowed home page returns a free robots_disallowed row. The engine identifies as TidyTools, never logs in or solves CAPTCHAs. Advanced JSON can override this setting.

## `roleEmailsOnly` (type: `boolean`):

Off (default): include business emails published on the listed website, including personal-looking addresses. On: keep only shared role mailboxes such as info@ or sales@. Do not use this Actor to make person-based lists.

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

The same as "Websites or company names", as a URL list; you can also upload or link a text/CSV file of websites. Bare domains belong in the list above (this editor rejects them).

## `namesCountry` (type: `string`):

2-letter country code (e.g. DE, GB, JP) so that company names are first tried on that country's domains (.de, .co.uk, .co.jp) before .com. Empty = .com, .io, .co.

## `datasetField` (type: `string`):

Field (or "rows" column) with the website or domain, e.g. "website" (dot paths such as "company.url" work). Empty = found automatically: website, then url, domain, link... A field that only holds Google Maps links (the "url" of a Maps place) is skipped, so Google Maps results need no setting.

## `datasetKeepFields` (type: `array`):

Extra fields copied from each dataset item into the output's "source" object, e.g. totalScore, reviewsCount. Google Maps results already get flat place columns (placeName, placeAddress, placePhone, mapsUrl, placeId, placeCategory, placeCity, placeCountryCode) without this setting.

## `mapsData` (type: `string`):

Alternative to rows or datasetId: paste a Maps JSON array, an object with items, or CSV with a website column. A datasetId takes precedence over sample pasted data.

## `mapsPlaceRows` (type: `boolean`):

Return CRM-ready place columns and preserve each original Maps item in source. Default off leaves this Actor's existing output unchanged.

## `phoneField` (type: `string`):

Field with the phone you already have (found automatically). The found phone is only put in "fill" when it is empty.

## `dedupeStoreName` (type: `string`):

Any name, e.g. "my-lead-list". Websites (by domain) read in an earlier run with the same name are not read or charged again: the earlier result is returned with fromPreviousRun. Kept in a key-value store "contacts-<name>" in your Apify account. Failed websites are not kept, so they are tried again. Empty = off.

## `dedupeMaxAgeDays` (type: `integer`):

With a dedupe name: results older than this are read again. 0 = no age limit.

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

The home page plus likely contact pages (contact, imprint/Impressum, about, team, support, legal). When those show no email, pages left over go to other pages likely to name a mailbox (policies, catering, jobs, locations...). The price per website is the same however many pages are read.

## `excludePaths` (type: `array`):

Optional JSON array of site paths beginning with /, e.g. {"excludePaths":\["/blog","/privacy"]}; enter one per line in the form. /blog excludes /blog/... but not /blogger; matching ignores case and query strings. Home pages and URLs you explicitly give are still read. Empty means no exclusion. A non-path entry causes a free invalid_input row and stops reading.

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

How many websites are processed at the same time (each reads up to 2 pages at once).

## `siteTimeoutSecs` (type: `integer`):

After this time no further pages of a website are started and the pages read so far are returned (skipped pages are listed in timedOutPages). If even the home page is not read by then, the website fails (not charged).

## `httpVia` (type: `string`):

Some sites block requests from data centers. Auto retries from a second network before falling back to a real browser. If those routes are refused, Auto also tries our second server (Oracle, different IP) before a real browser.

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

Only used for requests sent from Apify's network. Apify proxy usage is billed to your Apify account.

## Actor input object example

```json
{
  "rows": [
    {
      "row_number": 1,
      "Website": "hofbraeuhaus.de",
      "Email": ""
    }
  ],
  "onlyMissingEmail": true,
  "requiredContact": "email",
  "domains": [],
  "outputMode": "sites",
  "findWebsitesForNames": false,
  "respectRobotsTxt": true,
  "roleEmailsOnly": false,
  "mapsPlaceRows": false,
  "dedupeMaxAgeDays": 90,
  "maxPagesPerSite": 8,
  "maxConcurrency": 10,
  "siteTimeoutSecs": 120,
  "httpVia": "auto"
}
```

# Actor output Schema

## `overview` (type: `string`):

No description

## `full` (type: `string`):

No description

## `failed` (type: `string`):

No description

# 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 = {
    "rows": [
        {
            "row_number": 1,
            "Website": "hofbraeuhaus.de",
            "Email": ""
        }
    ],
    "domains": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("tidytools/website-email-finder-crm").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 = {
    "rows": [{
            "row_number": 1,
            "Website": "hofbraeuhaus.de",
            "Email": "",
        }],
    "domains": [],
}

# Run the Actor and wait for it to finish
run = client.actor("tidytools/website-email-finder-crm").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 '{
  "rows": [
    {
      "row_number": 1,
      "Website": "hofbraeuhaus.de",
      "Email": ""
    }
  ],
  "domains": []
}' |
apify call tidytools/website-email-finder-crm --silent --output-dataset

```

## MCP server setup

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

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/ARXGZVaVxl12jbt1v/builds/PYSqkGaFimlqj7KSA/openapi.json
