# Contact Info Scraper & Email Extractor for Websites (`zovo-one/contact-info-email-extractor`) Actor

Contact info scraper and email extractor: give it company domains, get one row per website with emails, phone numbers, social profiles, company name and address. $5 per 1,000 websites with contacts found. Websites with nothing found and failed websites are free.

- **URL**: https://apify.com/zovo-one/contact-info-email-extractor.md
- **Developed by:** [Michael Lip](https://apify.com/zovo-one) (community)
- **Categories:** Lead generation, Automation, Marketing
- **Features:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$5.00 / 1,000 website with contacts

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

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

## Contact Info Scraper & Email Extractor for Websites

Give it a list of company websites and get one row per website with the emails, phone numbers, social profiles, company name and postal address the site publishes. The price is $5 per 1,000 websites where contacts were found ($0.005 each). Websites with nothing found and websites that fail are free, and the number of pages read per website does not change the price.

It reads the home page first, then the pages that usually hold contact details: contact, about, team, impressum, legal and support pages, and matching sitemap.xml entries. It uses plain HTTP requests with no browser, so a run of 1,000 websites finishes in minutes.

### What you get for each website

| Field | What it holds |
| --- | --- |
| `domain`, `finalUrl` | The domain you gave and the home page after redirects |
| `status` | `ok` (at least one email or phone, the only charged status), `no-contacts`, `blocked`, `dns-error`, `timeout`, `error` or `skipped` |
| `emails` | Each address with its `type` (`role` such as info@ or sales@, `personal`, or `generic`), the `source` page, and `mx` (the domain has mail servers: true, false, or null when unknown) |
| `phones` | Each number in E.164 format (+4930...) when the country is clear from the number or the site's country domain, otherwise as written, with the `source` page |
| `socials` | Profile links for LinkedIn, X (Twitter), Facebook, Instagram, YouTube, TikTok, GitHub, Threads, Telegram and WhatsApp |
| `companyName` | From og:site_name, the schema.org Organization, or the page title |
| `address` | The schema.org PostalAddress, when the site publishes one |
| `pagesFetched`, `errorCode`, `error`, `httpStatus` | What the crawl did, and why a website failed |

An example row (made-up company):

```json
{
  "domain": "litware-example.de",
  "finalUrl": "https://www.litware-example.de/",
  "status": "ok",
  "companyName": "Litware GmbH",
  "emails": [
    { "email": "info@litware-example.de", "type": "role", "source": "https://www.litware-example.de/kontakt", "mx": true }
  ],
  "phones": [
    { "phone": "+49301234567", "format": "e164", "source": "https://www.litware-example.de/impressum" }
  ],
  "socials": { "linkedin": ["https://www.linkedin.com/company/litware-example"], "x": [], "facebook": [], "instagram": [], "youtube": [], "tiktok": [], "github": [], "threads": [], "telegram": [], "whatsapp": [] },
  "address": { "street": "Beispielstr. 1", "locality": "Berlin", "postalCode": "10115", "country": "DE", "formatted": "Beispielstr. 1, 10115 Berlin, DE" },
  "pagesFetched": 6,
  "errorCode": null
}
```

### Price

- $0.005 per website that returned at least one email or phone number. That is $5 per 1,000 websites with contacts.
- Free: websites where nothing was found, blocked websites, DNS errors, timeouts and bad input lines. Each one still comes back as a row with a reason.
- One price per website, whatever the number of pages read (1 to 30).
- Set a maximum charge for the run, and the Actor stops starting new websites when it is reached. Websites it skipped come back as free `SKIPPED_MAX_CHARGE` rows.

Example: 1,000 websites where 650 have contacts cost $3.25. A per-page price of $0.002 on the same 1,000 websites at 8 pages each would be $16.

### How it finds contacts

- Emails from `mailto:` links, the visible text and JSON-LD. It decodes common hiding tricks: `name [at] site [dot] com`, `(at)`, HTML entities and Cloudflare email protection.
- It drops things that only look like emails: image names such as `logo@2x.png`, placeholders such as `you@example.com`, and tracking keys from error-reporting tools.
- Phones from `tel:` links first, then from text right after a phone label (Phone, Tel, Telefon, Call). Fax numbers, dates, prices and loose digit runs are skipped.
- Social profiles from links only. Share buttons, intent links and single posts are dropped, and profile URLs are cleaned up.
- The MX check runs once per email domain per run. It is a DNS lookup, and no email is sent.
- robots.txt is read for each website, and disallowed paths are skipped. A website that disallows its home page comes back as `blocked` with `ROBOTS_DISALLOWED`.

### Input

| Field | Default | Notes |
| --- | --- | --- |
| `domains` | two examples | Websites, one per line: `acme.com`, `www.acme.com` or `https://acme.com/contact`. Up to 1,000 per run, duplicates are crawled once |
| `startUrls` | empty | Same as `domains`, for lists from a file or another Actor |
| `maxPagesPerDomain` | 8 | 1 to 30, home page included |
| `sameDomainOnly` | true | Follow only links on the same domain and its subdomains |
| `includePhones`, `includeSocials`, `checkMx` | true | Turn off what you do not need |
| `maxConcurrency` | 10 | Websites crawled in parallel, 1 to 50 |
| `timeoutSecs` | 20 | Per page, 5 to 60 |

### Use it from code

With curl, run it and get the rows back in one call:

```bash
curl -X POST "https://api.apify.com/v2/acts/zovo-one~contact-info-email-extractor/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"domains": ["apify.com", "python.org"], "maxPagesPerDomain": 8}'
```

With Python (`pip install apify-client`):

```python
from apify_client import ApifyClient

client = ApifyClient("YOUR_APIFY_TOKEN")
run = client.actor("zovo-one/contact-info-email-extractor").call(
    run_input={"domains": ["apify.com", "python.org"], "maxPagesPerDomain": 8}
)
for row in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(row["domain"], row["status"], [e["email"] for e in row["emails"]])
```

The run also writes an `OUTPUT` record with counts: websites with contacts, without contacts, failed, and charged.

Price examples, a sample output row, the limits and common questions are also on [the Zovo page for this Actor](https://theluckystrike.github.io/zovo-plugin-factory/apify/contact-info-email-extractor/).

### What it does not do

- No login pages and no pages behind a login.
- No LinkedIn or other social network scraping. Social profile links are read from the company's own website only.
- No guessing of email patterns (such as first.last@) and no email sending or verification beyond the MX lookup.
- No browser. Websites that need JavaScript to show their contacts, or that block plain HTTP clients with a bot check, come back as `no-contacts` or `blocked`, and you are not charged.
- No proxies in this version. Requests come from Apify's servers.

### Lawful use

This Actor collects business contact details that companies publish on their own websites. You are responsible for how you use them: data protection laws such as the GDPR, and email rules such as CAN-SPAM, may apply to storing the data and to contacting people. Personal addresses of named employees can be personal data. Keep only what you need, and honour opt-outs.

### FAQ

#### Why does a big website come back as blocked?

Some large websites block any client that is not a full browser. The row says `BLOCKED_BY_SITE` and is free. Most small and mid-size company websites answer plain HTTP.

#### Why are the rows in a different order than my list?

Websites are crawled in parallel and rows land as they finish. Match them on `input` or `domain`.

#### Can I run it on a schedule?

Yes. Use an Apify schedule or task with a fixed list. Each run is priced the same way.

### Open-source parts

HTTP comes from [undici](https://github.com/nodejs/undici) (MIT). The HTTP, address-guard and charset modules are shared with the Zovo [URL to Markdown web fetch Actor](https://apify.com/zovo-one/web-fetch-fallback). This Actor's own code is MIT licensed.

[zovo-support-start]: #

### Support Zovo

Zovo builds and maintains this Actor. If it saves you time, you can support the work on [GitHub Sponsors](https://github.com/sponsors/theluckystrike). Sponsorship is optional, and the tool works the same without it.

You can also use [PayPal](https://www.paypal.com/paypalme/najzzz).

Zovo is made by [theluckystrike](https://github.com/theluckystrike) on GitHub.

#### Need your format supported?

Open a [format request](https://github.com/theluckystrike/zovo-plugin-factory/issues/new?template=format-request.yml) with a sample file, and I reply there whether it can be done. For USD 299 once, through [this GitHub Sponsors tier](https://github.com/sponsors/theluckystrike/sponsorships?tier_id=679995), I add your file format or a missing feature to a Zovo VS Code viewer, with tests, within 7 days.

### More tools by Zovo

- [App Store and Google Play Reviews](https://apify.com/zovo-one/app-store-google-play-reviews). Gets App Store and Google Play reviews in one run, with one schema.
- [Website Screenshot API](https://apify.com/zovo-one/website-screenshot-pdf-api). Saves full-page screenshots and PDFs of public web pages.
- [YouTube Transcript Scraper](https://apify.com/zovo-one/tubetext-youtube-transcript-scraper). Gets YouTube transcripts and captions from videos, playlists and channels.
- [Google Trends Scraper](https://apify.com/zovo-one/google-trends-scraper-fast). Gets Google Trends interest over time, by region and related queries, with no browser.

Free VS Code extensions by Zovo for the files you export:

- [Large JSON Viewer, JSON Browser & JSON Tree](https://marketplace.visualstudio.com/items?itemName=zovo.zovo-json-tree-viewer). Shows large JSON files as a searchable tree, copies JSONPath and turns arrays of objects into a table.
- [XLSX Viewer, Excel Viewer, CSV Viewer & Parquet Viewer (XLS, CSV Preview)](https://marketplace.visualstudio.com/items?itemName=zovo.table-viewer-vscode). Opens CSV, TSV, Excel and Parquet files as a sortable, filterable table.
- [JSONL Viewer, Large JSONL Table & NDJSON Viewer](https://marketplace.visualstudio.com/items?itemName=zovo.zovo-jsonl-viewer). Opens JSONL and NDJSON files as a table, as records or as a chat view.

Zovo tools: [all Zovo Actors on Apify](https://apify.com/zovo-one), [price examples for each Actor](https://theluckystrike.github.io/zovo-plugin-factory/apify/) and [the free Zovo VS Code extensions](https://marketplace.visualstudio.com/publishers/zovo).

[zovo-support-end]: #

# Changelog

This Actor's version history is a separate document: https://apify.com/zovo-one/contact-info-email-extractor/changelog.md

# Actor input Schema

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

Company websites to scan, one per line, up to 1,000 per run. A bare domain (acme.com), a host (www.acme.com) or a full URL (https://acme.com/contact) all work. Each domain is crawled once and returns one row. Private, local and reserved addresses are refused.

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

Another way to give websites, for example from a file or another Actor's output. They are merged with the list above.

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

Pages read per website, home page included (1 to 30). The Actor reads the home page, then the pages most likely to hold contacts: contact, about, team, impressum, legal and support pages, in that order, and matching sitemap.xml entries. The price per website is the same whatever this is.

## `sameDomainOnly` (type: `boolean`):

On: follow only links on the same domain and its subdomains. Off: also follow contact links to other domains.

## `includePhones` (type: `boolean`):

Collect phone numbers from tel: links and from text next to a phone label. Numbers are in E.164 format (+4930...) when the country is clear, and as written otherwise.

## `includeSocials` (type: `boolean`):

Collect LinkedIn, X (Twitter), Facebook, Instagram, YouTube, TikTok, GitHub, Threads, Telegram and WhatsApp profile links.

## `checkMx` (type: `boolean`):

Check once per email domain that it has mail servers (DNS MX). Each email then has mx true, false, or null when the lookup failed. No email is sent.

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

How many websites are crawled in parallel (1 to 50).

## `timeoutSecs` (type: `integer`):

How long to wait for one page (5 to 60).

## Actor input object example

```json
{
  "domains": [
    "https://apify.com",
    "https://www.python.org"
  ],
  "maxPagesPerDomain": 8,
  "sameDomainOnly": true,
  "includePhones": true,
  "includeSocials": true,
  "checkMx": true,
  "maxConcurrency": 10,
  "timeoutSecs": 20
}
```

# Actor output Schema

## `contacts` (type: `string`):

No description

## `errors` (type: `string`):

No description

## `allFields` (type: `string`):

No description

## `runSummary` (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 = {
    "domains": [
        "https://apify.com",
        "https://www.python.org"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("zovo-one/contact-info-email-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 = { "domains": [
        "https://apify.com",
        "https://www.python.org",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("zovo-one/contact-info-email-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 '{
  "domains": [
    "https://apify.com",
    "https://www.python.org"
  ]
}' |
apify call zovo-one/contact-info-email-extractor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,zovo-one/contact-info-email-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/ccBpC0L2nxs95G7Vx/builds/ZRsTJdHehoKtQuvDh/openapi.json
