# Company Website Contact Info Scraper (`sunpeng/company-website-contact-info`) Actor

Extract emails, phone numbers and social links from company websites or domain lists. Respects robots.txt, CRM-ready output, MX check, $1 per 1,000 pages, failed pages free.

- **URL**: https://apify.com/sunpeng/company-website-contact-info.md
- **Developed by:** [Ye Qiangwei](https://apify.com/sunpeng) (community)
- **Categories:** Lead generation, Automation
- **Stats:** 2 total users, 1 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 successfully parsed html pages

This Actor is paid per event and usage. You are charged both the fixed price for specific events and for Apify platform usage.

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

## Company Website Contact Scraper — Emails, Phone Numbers & CRM Export

Turn a list of company websites into source-linked public email addresses, phone numbers and social profile links. Each company is summarized in one dataset row, ready for review or CRM enrichment. Choose `perContact` for a flat contact export.

### What you get

- Public email addresses, phone numbers and social profile links found on the company pages you supply.
- Contact, About, Legal, Team and Support page discovery within each company’s registered domain.
- Source URLs for each contact, plus page results and company status for review.
- Company rows with ready-to-export CRM fields, or one row per contact with `perContact`.
- Configurable page and spending limits, with fees based on successfully parsed HTML pages.

### Who is it for?

Sales operations teams enriching company lists, agencies reviewing client contact details, and researchers organizing public business directories. Export `primaryEmail`, `primaryPhone`, `contactPageUrl`, `emailsFlat` and `phonesFlat` directly to CSV for CRM review.

The Actor reads static HTML. It does not render JavaScript or verify that an email address exists. Coverage depends on what each website publishes and allows the Actor to access.

### Pricing

| Event | Price (USD) | When charged |
| --- | ---: | --- |
| `run-start` | $0.00005 per run (nominal startup fee) | Once for a validated new run, including runs with failed websites or no contacts |
| `page-fetched` | $0.001 per page ($1.00 / 1,000 pages) | Once for each successfully fetched and parsed HTML page, **including pages without contacts** |

There is no per-contact or dataset-row event fee. Finding ten contacts on one page still costs one page event. The homepage counts toward both the page budget and the price.

Robots requests, redirect hops, duplicate final pages, HTTP failures, non-HTML responses, login pages, protection challenges, timeouts and parsing failures do not incur page events.

For **100 companies × about 8 successful pages each = 800 pages**, one run costs $0.00005 + 800 × $0.001 = **$0.80005 (about $0.80)**. Actual successful page counts determine the price.

| Other single-run examples | Actor event fees |
| --- | ---: |
| 8 successful pages | **$0.00805** |
| 1,000 successful pages | **$1.00005** |
| All websites fail, with enough budget to start | **$0.00005** |

These examples cover Actor event fees. See the Store pricing panel for the effective price and any other platform charges.

#### Spending limits and billing status

Set the run’s maximum total charge to control spending. The Actor uses the lower of that limit and `ACTOR_MAX_TOTAL_CHARGE_USD`, after subtracting charges already known to the SDK, including any automatic platform startup event. It reserves capacity before fetching each page and stops scheduling pages when the remaining budget cannot cover the next event.

Before any automatic platform charges, the minimum Actor event budget is **$0.00005** to start, or **$0.00105** for startup plus one successful page. A budget between those amounts permits only the startup event, with no page requests. Zero budget permits no events; fractions smaller than one microdollar are rounded down.

All companies remain in the output when billing stops:

- Companies with pending pages, or not yet started, receive `CHARGE_LIMIT` for budget exhaustion or `BILLING_STOPPED` for a billing/storage failure. Their status is `partial` if a page was fetched, otherwise `skipped`.
- `limits.chargeLimitReached` is true only when budget exhaustion affects that company. Completed companies keep their original status and flags. Independently blocked companies keep `blocked`.
- The key-value store record `SUMMARY` contains the global stop reason. Check page-level `billing` for uncertain charges: a company can retain its extraction status if its last page’s charge is uncertain and no pages remain.

An uncertain charge or receipt-storage failure stops further charging; uncertain events are never retried. Receipts are saved in batches to `CHARGE-CHECKPOINT`. Forced termination can leave the latest receipts or company output unsaved, so checkpoints do not replace platform billing records. Automatic migration/restart using existing run storage is refused to prevent duplicate charging.

### Input

Provide exactly one of `startUrls` or `domains`, containing 1–1,000 entries. URLs may include a path: `https://example.org/contact` starts at that exact page; the homepage is not automatically added. URLs sharing a registered domain are combined into one company and share one page budget. The public suffix list, including private suffixes, keeps independent tenants such as `a.github.io` and `b.github.io` separate.

```json
{
  "startUrls": [
    { "url": "https://example.com/" },
    { "url": "https://example.org/contact" }
  ],
  "maxPagesPerDomain": 8,
  "maxConcurrency": 5,
  "includeSubdomains": true,
  "requestIntervalMs": 1000,
  "globalIntervalMs": 100,
  "timeoutMs": 15000,
  "outputMode": "perCompany",
  "checkMx": false,
  "defaultCountry": "GB"
}
```

Alternatively:

```json
{
  "domains": ["example.com", "example.org"],
  "outputMode": "perContact"
}
```

Bare domains use HTTPS, with no automatic HTTP or `www` fallback. JSON/API input also accepts URL strings in `startUrls`; the Store URL editor uses `{ "url": "..." }` objects.

Only the URL is used. Standard request-list fields such as `uniqueKey`, `userData` and `headers` are accepted but ignored; `method`, if supplied, must be `GET`. Request-list files, credentials, login paths, cookies and proxy options are unsupported.

| Option | Default | Limits / behavior |
| --- | --- | --- |
| `maxPagesPerDomain` | 8 | 1–30 **page candidates examined**, including failed or robots-disallowed candidates; includes supplied seed pages |
| `maxConcurrency` | 5 | 1–20 concurrent company workers; one HTTP request at a time per registered domain |
| `includeSubdomains` | `true` | Follow relevant links and redirects within the registered domain. If `false`, allow only hostnames explicitly provided in the input, including for robots redirects; company grouping stays by registered domain |
| `requestIntervalMs` | 1,000 | 100–60,000 ms between request starts for a company; robots Crawl-delay takes precedence |
| `globalIntervalMs` | 100 | 20–10,000 ms between global request starts, including robots and redirect hops |
| `timeoutMs` | 15,000 | 1,000–60,000 ms per DNS lookup and per HTTP hop, including body transfer; separate from scheduling delays |
| `outputMode` | `perCompany` | `perCompany` or `perContact` |
| `checkMx` | `false` | DNS MX lookup only, cached per email domain, with a 3-second lookup timeout |
| `defaultCountry` | unset | Optional uppercase country code such as `GB` or `US` for national phone numbers; no country is guessed |

After the supplied pages, the Actor follows only linked pages with relevant anchor text or paths. It does not guess contact paths or open arbitrary news/product pages. It queues at most 3,000 relevant links per company and examines no more than the configured page budget.

Matching covers common English phrases such as “get in touch”, “reach us” and “enquiries”, plus selected French, Spanish, Italian, Portuguese, Dutch, German, Chinese and Japanese terms. This is a limited keyword list, not full language coverage or translation. Uncommon labels may be missed; broad words such as “connect” and “feedback” alone do not qualify.

### Output example

One illustrative company row for a fictional business (not collected from a live website):

```json
{"domain":"northstar-studio.co.uk","companyName":"Northstar Studio Ltd","primaryEmail":"hello@northstar-studio.co.uk","primaryPhone":"+442079460958","contactPageUrl":"https://northstar-studio.co.uk/contact","emailsFlat":"hello@northstar-studio.co.uk;sales@northstar-studio.co.uk","phonesFlat":"+442079460958","pagesFetched":8,"pagesFailed":0,"status":"succeeded"}
```

The compact example selects CRM columns. Full company rows also contain nested contacts, page evidence and billing details as shown below.

| CSV column | Meaning |
| --- | --- |
| `domain`, `companyName` | Registered company domain and published organization name; name may be empty |
| `primaryEmail` | First general business/role email by source priority, falling back to a suspected personal address; empty if none |
| `primaryPhone` | First observed phone by source priority; may be a national number when no country was provided |
| `contactPageUrl` | Best fetched contact-path page, falling back to a prioritized page with observed contacts; empty if none |
| `emailsFlat`, `phonesFlat` | Deduplicated values joined with semicolons, ordered with the primary value first; empty strings if none |
| `status`, `pagesFetched`, `pagesFailed` | Company outcome and examined-page results; check limitations before treating missing data as absent |

Export the default `perCompany` Dataset as CSV to use these columns. `primaryEmail`, `primaryPhone` and `contactPageUrl` are JSON `null` when unavailable. Phone extensions stay in the phone value.

Source priority is Contact > Impressum/Legal/Privacy > About/Company > Team > Support > homepage > other paths, with the contact value breaking ties. Role emails always precede suspected personal addresses. This ranking does not establish who is best to contact. Flat fields include suspected personal addresses too; review them before import.

Full illustrative row:

```json
{
  "domain": "acme-company.com",
  "companyName": "Acme Company",
  "primaryEmail": "info@acme-company.com",
  "primaryPhone": "+442079460958",
  "contactPageUrl": "https://acme-company.com/contact",
  "emailsFlat": "info@acme-company.com",
  "phonesFlat": "+442079460958",
  "emails": {
    "general": [
      {
        "value": "info@acme-company.com",
        "sourcePages": ["https://acme-company.com/contact"]
      }
    ],
    "personal": []
  },
  "phones": [
    {
      "value": "+442079460958",
      "e164": "+442079460958",
      "extension": null,
      "raw": "+44 20 7946 0958",
      "sourcePages": ["https://acme-company.com/contact"]
    }
  ],
  "socialLinks": [
    {
      "value": "https://www.linkedin.com/company/acme-company",
      "sourcePages": ["https://acme-company.com/contact"]
    }
  ],
  "sourcePages": [
    {
      "requestedUrl": "https://acme-company.com/contact",
      "url": "https://acme-company.com/contact",
      "status": "fetched",
      "billing": "confirmed",
      "httpStatus": 200,
      "redirectCount": 0,
      "contacts": [
        { "type": "email", "value": "info@acme-company.com" },
        { "type": "phone", "value": "+442079460958" },
        { "type": "social", "value": "https://www.linkedin.com/company/acme-company" }
      ]
    }
  ],
  "pagesFetched": 1,
  "pagesFailed": 0,
  "status": "succeeded",
  "failureReasons": [],
  "scrapedAt": "2026-10-23T00:00:00.000Z",
  "billing": { "mode": "platform", "pagesConfirmed": 1, "pagesUnknown": 0 },
  "limits": { "maxPagesPerDomain": 8, "pageLimitReached": false, "chargeLimitReached": false },
  "mxDisclaimer": null
}
```

Each contact has its own `sourcePages` URL list. The company’s top-level `sourcePages` holds page outcomes, contact references and billing confirmations. Fetched pages also include `parsedAt`, `chargeReceiptId` and `chargeCheckpointKey` to link them to billing receipts.

`companyName` comes from explicit organization JSON-LD or `og:site_name` when available; it may be null.

General role addresses such as `info@`, `sales@`, `support@` and `contact@` are separated from **suspected** personal addresses. This is a local-part heuristic, not identity verification. The Actor only extracts addresses actually published on the supplied company's pages; it never guesses employee addresses. Contacts are deduplicated within a company and retain all observed source pages. The same contact can appear for different companies.

Phone numbers are validated and normalized using numbering-plan metadata. National numbers require `defaultCountry`; otherwise plausible numbers found in explicit `tel:` links are retained with `e164: null`. Extensions are preserved. A valid format does not establish ownership or whether a phone is active.

Common `[at]`, `(at)`, `[dot]` and Cloudflare email-protection XOR encodings are decoded locally. Scripts, styles, image filenames, placeholder/test domains, no-reply addresses and common telemetry addresses are filtered. This does not solve CAPTCHAs or bypass access controls.

With `checkMx: true`, each email includes `mx.status` (`present`, `no_mx`, `null_mx` or `unknown`), `exchanges` and `deliverability: "not_verified"`. **MX presence does not mean an address exists or is deliverable.** No SMTP connection, message sending or mailbox probing occurs. A missing MX may still permit SMTP fallback; this is an observation, not a deliverability verdict.

`perContact` produces rows such as:

```json
{
  "domain": "acme-company.com",
  "companyName": "Acme Company",
  "contactType": "email",
  "category": "general",
  "value": "info@acme-company.com",
  "sourcePages": ["https://acme-company.com/contact"],
  "status": "succeeded",
  "pagesFetched": 1,
  "pagesFailed": 0
}
```

Flat rows also include the five CRM fields, company metadata, `failureReasons`, `billing`, `limits` and `scrapedAt`.

- `sourcePages` always contains that contact’s source URL list.
- `pageResults` contains the company’s page outcomes (the top-level `sourcePages` in `perCompany` output).
- Company counters repeat on each contact row: **do not sum them across contacts**.
- Companies with no contacts retain a row with `contactType: null`, `value: null`, `sourcePages: []` and the company’s page evidence in `pageResults`. If no pages were attempted, `pageResults` is also empty.

#### Understanding results

| Status | Meaning |
| --- | --- |
| `succeeded` | Contacts were found within the examined pages, without a reported incomplete condition |
| `empty` | Examined pages produced no contacts, without a reported incomplete condition |
| `partial` | Failed/skipped candidates, the page budget or a charge stop left the company incomplete |
| `failed` | No page was successfully fetched and a page failure was reported |
| `blocked` | Access was refused; may retain contacts collected before the refusal |
| `skipped` | The company was not fetched, for example because the spending limit was reached before it started |

No status guarantees that every website contact was found. `pagesFetched` counts parsed HTML pages, including empty pages. `pagesFailed` counts failed examined candidates, including robots exclusions. Robots requests and redirect hops are excluded from both counters. Check page-level billing for uncertain charge outcomes.

### Access rules and limitations

#### Website access

The Actor reads each origin’s robots.txt as `CompanyContactsBot`. Disallowed paths and unavailable or ambiguous rules are skipped conservatively. A 404/410 allows normal evaluation. Transient network failures and HTTP 5xx receive at most one robots retry per origin per run, through the same rate limits; deterministic refusals are cached without retry. Crawl-delay above 60 seconds results in a skip.

HTTP 401/403/429, detected login walls and protection challenges stop that company without retries or IP changes. Other page failures are reported without retrying. The Actor sends no target cookies or credentials, uses no proxies, submits no forms, and does not execute JavaScript, load images or follow social profiles.

Pages and robots fetches allow up to five redirect hops, within the original registered domain. Every destination is checked for private/reserved addresses, mixed public/private DNS answers are rejected, and connections are pinned to the validated address. Off-domain redirects are reported as incomplete.

#### Coverage and resource limits

- Contacts must appear in returned HTML. JS-only data, images of contact details, advanced obfuscation and details requiring interaction may be missed.
- Social links come from a known host list; unrecognized hosts may be omitted. Language matching, filtering, phone parsing, company names and challenge detection are heuristic and may miss data or produce false positives.
- Compressed and decoded HTML are each limited to **5 MiB**; robots.txt is limited to **512 KiB**. gzip, deflate and Brotli are supported. Unsupported/binary/invalid character encodings fail without page fees; ordinary imperfect HTML is accepted.
- Default memory is **1,024 MiB** (minimum 512 MiB). Large pages and higher concurrency increase memory use; these settings do not guarantee against out-of-memory failures.
- Cross-run change and deletion tracking are not supported.

#### Responsible use

Supply only public company pages you are authorized to collect. Review suspected personal addresses before use and apply appropriate retention and deletion rules. Public availability and robots.txt access do not establish permission to republish information or contact people; comply with website terms and applicable privacy and marketing rules.

### FAQ

**Does it support JavaScript websites?** No JavaScript is executed. Static HTML returned by a JavaScript-based site can be parsed, but contacts rendered only by scripts or interaction are unsupported.

**Does it respect robots.txt?** Yes. Each origin is checked with `CompanyContactsBot`; disallowed paths and unavailable or ambiguous robots responses are skipped conservatively. HTTP 401/403/429 and detected access challenges stop the company without retries.

**Are emails verified using SMTP?** No. The optional MX check uses DNS only, never connects to SMTP, sends messages or probes a mailbox. MX presence does not verify an address or deliverability.

**Who is responsible for personal data and outreach compliance?** You are responsible for your lawful basis, permissions, privacy notices, retention and deletion, and compliance with applicable rules such as GDPR and CAN-SPAM. Public contact details can still be personal data. Review the source and intended use before exporting, storing or contacting anyone; this Actor does not establish legal permission.

# Actor input Schema

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

HTTP(S) URLs may include a path: crawling starts at that exact page, without adding the homepage. Strings or {url} objects. Seeds sharing a registered domain are merged. Do not combine with domains.

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

Bare company host names (HTTPS is used). Use startUrls for HTTP or a specific page. Do not combine with startUrls.

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

Maximum page candidates examined per registered domain, including failed or robots-disallowed candidates. Robots and redirect hops are separate uncharged requests.

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

Default 5 concurrent company workers (1–20). Each registered-domain company performs one HTTP request at a time.

## `requestIntervalMs` (type: `integer`):

Includes robots and redirect hops; a longer robots Crawl-delay takes precedence.

## `globalIntervalMs` (type: `integer`):

Spaces request starts across all companies.

## `timeoutMs` (type: `integer`):

Per DNS lookup and per HTTP hop, including body transfer; not a total crawl timeout.

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

perCompany aggregates contacts; perContact flattens contacts and preserves an empty row for companies without contacts.

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

DNS only. No SMTP, no email sent. MX presence does not verify deliverability.

## `defaultCountry` (type: `string`):

Optional uppercase ISO country code, e.g. GB or US. Used for national numbers; no country is guessed from the website.

## `includeSubdomains` (type: `boolean`):

Default true: follow relevant pages and redirects within the registered domain. False: restrict each company to explicitly supplied seed hostnames (including robots redirects); company grouping stays by registered domain.

## Actor input object example

```json
{
  "maxPagesPerDomain": 8,
  "maxConcurrency": 5,
  "requestIntervalMs": 1000,
  "globalIntervalMs": 100,
  "timeoutMs": 15000,
  "outputMode": "perCompany",
  "checkMx": false,
  "includeSubdomains": true
}
```

# Actor output Schema

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

No description

## `summary` (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 = {};

// Run the Actor and wait for it to finish
const run = await client.actor("sunpeng/company-website-contact-info").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 = {}

# Run the Actor and wait for it to finish
run = client.actor("sunpeng/company-website-contact-info").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 '{}' |
apify call sunpeng/company-website-contact-info --silent --output-dataset

```

## MCP server setup

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

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/y7NXaSdgqsb4TjUxh/builds/zf5L6U82uGYS4A162/openapi.json
