# Company Enrichment from Website: Emails, Phones & Tech (`cleanyield/company-enrichment-from-website`) Actor

Email extractor and company enrichment by domain: one clean row per company with emails (generic vs personal), E.164 phones, socials, tech stack, hiring signals and latest posts. Fixed price per company, hard page and time cap per site, failed sites never charged.

- **URL**: https://apify.com/cleanyield/company-enrichment-from-website.md
- **Developed by:** [Clean Yield](https://apify.com/cleanyield) (community)
- **Categories:** Lead generation, Developer tools, AI
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $7.00 / 1,000 companies

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

## Company Enrichment from Website: Emails, Phones & Tech

**You only pay for valid, complete results** — one fixed price per company, never per page. Sites that are down, parked or blocked are reported and never charged.

#### Switching from another scraper? Paste your current input as is

`startUrls` (`[{ "url": "https://acme.com" }]`), `urls` and `maxRequestsPerStartUrl` are accepted unchanged, and `outputFormat: "leader-compatible"` returns the usual contact-scraper field names (`emails`, `phones`, `linkedIns`, `twitters`, `whatsapps`...) next to ours. Your integration keeps working; the bill becomes one fixed price per company.

| Typical field | Here |
|---|---|
| `startUrls[].url` / `urls` | `domains` (both accepted) |
| `maxRequestsPerStartUrl` / `maxPagesPerSite` | `maxPagesPerDomain` |
| `emails`, `phones` (string lists) | `emails[]`, `phones[]` (classified, E.164) + `primaryEmail`, `primaryPhone` |
| `linkedIns`, `twitters`, `facebooks`, `instagrams`... | `socialProfiles.linkedin`, `.x`, `.facebook`, `.instagram`... |

Full mapping in *Migrating from other scrapers*.

**Email extractor and company enrichment by domain.** Turn a list of domains or website URLs into **one clean row per company**: generic and role emails (info@, sales@, ventas@), phone numbers in E.164 (WhatsApp marked), LinkedIn/X/Facebook/Instagram/YouTube/TikTok/GitHub/Pinterest/Threads/Telegram profiles, tech stack (Shopify, HubSpot, WordPress, Intercom...), hiring signals (careers page, ATS, open roles) and the latest blog posts. Built for lead-gen agencies, SDR teams and Clay / n8n / Make users who enrich account lists. Also works as a **website contact details scraper**, a **lead scraper** for emails, phone numbers and social media links, a **domain enrichment API**, a **tech stack lookup** and a **"is this company hiring?" checker**.

Want to try first? Use the free Lite version: https://apify.com/cleanyield/company-enrichment-from-website-lite

### Why use this email extractor and company enrichment tool?

- 📇 **More contacts, no junk**: 86% of companies with at least one email and 58% with a phone on our fixed test set of real sites, from contact, legal notice/Impressum, privacy, about and team pages in 6 languages, page footers, structured data and obfuscated emails — every address validated (real TLD, no placeholders, no regulators or agencies).
- ✅ **One row per company, fixed price, no runaway runs**: no surprise bills from crawling thousands of pages of one site. A hard cap of 10 pages and 15 seconds per domain that is always enforced, the maximum cost printed before the run starts, and nothing charged for sites that fail.
- ☎️ **Company phone numbers that actually show up**: read from contact, legal notice, footer and structured data, normalized to E.164 with country, deduplicated across formats and with junk (IDs, years, test numbers) filtered out.
- 🧹 **Clean, normalized output**: phones in E.164 (deduplicated across formats, vanity numbers like 1-800-4HOTDOG converted), emails classified as `generic` / `role` / `personal`, Cloudflare-protected emails decoded, junk removed (`posthog-js@1.8.0`, `logo@2x.png`, `555-01xx`, `+1 234 567 8910`).
- 📈 **Buying signals, not just contacts**: is the company hiring (and on which ATS, with open roles counted from Greenhouse, Lever and Ashby public APIs), which tools it uses, what it published last.
- 🔁 **Built for monitoring**: schedule it and get only companies that changed — started hiring, added HubSpot, new post, new contact.
- ⚡ **Fast**: median 1.8 s per company (p90 3.1 s, max 5.9 s) on 64 real company websites; sync mode for Clay, n8n and Make.
- 🎯 **Pro mode in one checkbox**: buying-intent score 0–100, estimated size, fit with your ideal customer, a "why now" line and a one-sentence AI insight per company.
- 🔒 **GDPR-friendly by default**: personal emails are omitted unless you turn them on; robots.txt respected; bot challenges never bypassed.
- 🤖 **AI-agent ready**: pay-per-result only (no usage fees), works through MCP without an Apify account.

#### This Actor vs typical alternatives

Measured head-to-head on 2026-11-05: this Actor and a widely used contact scraper, run on Apify with the same 10 real domains from our fixed test set (`testset/cases.json`: SaaS, restaurants, ecommerce, LATAM), same 10-page budget per site. Our full-set numbers (65 domains) are in "Measured quality".

| Same 10 domains, same page budget | Typical alternative (measured) | This Actor |
|---|---|---|
| Output | one row per site (merged contacts) | **one row per company + name, country, tech, hiring** |
| Expected fields filled (68 checks) | 47% | **100%** |
| Contact fields correct (emails, phones, socials) | 79% | **100%** |
| Companies with at least one email / confirmed phone | 8 / 2 of 10 | **10 / 6 of 10** |
| Unique emails found | 11 | **27** |
| Phones normalized (E.164), junk filtered | ✗ (mixed formats, IDs and years as "uncertain" phones) | **✓** |
| Run time for the 10 companies | 71 s | **5 s** |
| Cost per 1,000 companies | ~USD 16 (pay per page) | **USD 10, fixed** |
| Monitor mode (only changes) + alerts | ✗ | **✓** |

### What data can you get from a company website?

| Field | Example |
|---|---|
| `companyName` | Plausible Analytics (from og:site_name, ld+json Organization, title or domain — slogans and "Inc." removed) |
| `emails[]` | `{ "email": "hello@plausible.io", "type": "generic", "isCompanyDomain": true, "foundOn": "https://plausible.io/contact" }` |
| `phones[]` | `{ "phone": "+12122542246", "national": "(212) 254-2246", "country": "US", "source": "tel-link", "isWhatsApp": false }` |
| `primaryEmail`, `primaryPhone` | the best email (company-domain generic inbox first) and phone (E.164) — ready for a spreadsheet or CRM column |
| `contactPageUrl` | the contact page (or legal notice) that was read |
| `socialProfiles` | `linkedin`, `x`, `facebook`, `instagram`, `youtube`, `tiktok`, `github`, `pinterest`, `threads`, `telegram`, `discord`, `reddit` — canonical company URLs |
| `address` | from ld+json: street, locality, region, postal code, ISO country |
| `technologies[]` | `{ "name": "Shopify", "category": "ecommerce" }` — ~170 signatures in 14 categories |
| `hiring` | `{ "isHiring": true, "careersUrl": "https://job-boards.greenhouse.io/n26", "atsProvider": "greenhouse", "openRolesCount": 36, "evidence": "36 open role(s) on the public greenhouse job board" }` |
| `latestPosts[]` | `{ "title": "...", "url": "...", "publishedAt": "2026-10-22T10:00:00.000Z" }` |
| `language`, `country` | `en`, `EE` (ISO 639-1 / ISO 3166-1) |
| `sourcesChecked[]` | every request made (type, URL, HTTP status, ms) — full transparency |
| `changeType`, `changes[]` | monitor mode only: `new` / `changed` and what changed |
| `pro` | Pro mode only: `buyingIntentScore` (0–100), `buyingSignals[]`, `estimatedSize`, `icpFitScore`, `whyNow`, `trend` (see *Pro mode*) |
| `insight` | Pro mode only: one sentence written by AI from this row's facts only |
| `schemaVersion` | `1.1.0` — fields are only added, never renamed ([CHANGELOG](CHANGELOG.md)) |

#### What counts as a valid (charged) result

A company is charged only if its website could be read **and** the row has `domain` + `companyName` + at least **one contact datum** (email, phone, social profile or address) **or one signal** (careers page/ATS, a blog post, or a business technology beyond hosting/CDN). Everything else is free and listed with its reason in the `FAILED_DOMAINS` record of the run's key-value store:

| Status | Meaning |
|---|---|
| `invalid_domain` | the input is not a domain or URL |
| `unreachable` | DNS does not resolve, invalid TLS certificate, connection error or timeout |
| `blocked` | HTTP 401/403/429 or a bot challenge (Cloudflare and similar) — never bypassed |
| `not_found` | the home page returns 404/410 |
| `parked` | the domain is parked or for sale |
| `robots_disallowed` | robots.txt disallows the site for all crawlers |
| `no_data` | the site was read but had nothing useful |

#### Example output

A real item from a run of the default input (`sourcesChecked` and `latestPosts` shortened):

```json
{
  "domain": "plausible.io",
  "url": "https://plausible.io/",
  "companyName": "Plausible Analytics",
  "description": "Plausible is a lightweight and open-source Google Analytics alternative. Your website data is 100% yours and the privacy of your visitors is respected.",
  "language": "en",
  "country": "EE",
  "emails": [
    {
      "email": "hello@plausible.io",
      "type": "generic",
      "isCompanyDomain": true,
      "foundOn": "https://plausible.io/contact"
    },
    {
      "email": "jobs@plausible.io",
      "type": "role",
      "isCompanyDomain": true,
      "foundOn": "https://plausible.io/jobs/customer-success"
    },
    {
      "email": "privacy@plausible.io",
      "type": "role",
      "isCompanyDomain": true,
      "foundOn": "https://plausible.io/privacy"
    }
  ],
  "phones": [],
  "primaryEmail": "hello@plausible.io",
  "primaryPhone": null,
  "contactPageUrl": "https://plausible.io/contact",
  "socialProfiles": {
    "linkedin": "https://www.linkedin.com/company/plausible-analytics",
    "x": "https://x.com/PlausibleHQ",
    "facebook": null,
    "instagram": null,
    "youtube": null,
    "tiktok": null,
    "github": "https://github.com/plausible",
    "pinterest": null,
    "threads": null,
    "telegram": null,
    "discord": null,
    "reddit": null
  },
  "address": {
    "streetAddress": "Västriku tn 2",
    "locality": "Tartu",
    "region": null,
    "postalCode": "50403",
    "country": "EE",
    "formatted": "Västriku tn 2, Tartu, 50403, EE"
  },
  "technologies": [
    {
      "name": "Plausible",
      "category": "analytics"
    },
    {
      "name": "Jekyll",
      "category": "framework"
    }
  ],
  "hiring": {
    "isHiring": true,
    "careersUrl": "https://plausible.io/jobs/customer-success",
    "atsProvider": null,
    "openRolesCount": null,
    "evidence": "Careers page lists openings or asks to apply"
  },
  "latestPosts": [
    {
      "title": "Google Analytics pricing: Free GA4 and paid GA360 explained",
      "url": "https://plausible.io/blog/google-analytics-pricing",
      "publishedAt": "2026-10-22T10:00:00.000Z"
    },
    {
      "title": "Shopify, Squarespace and Webflow analytics: What you can track and what’s missing",
      "url": "https://plausible.io/blog/cms-built-in-analytics",
      "publishedAt": "2026-10-02T07:00:00.000Z"
    },
    {
      "title": "What is direct traffic? What does it include, and why is its share the highest?",
      "url": "https://plausible.io/blog/direct-traffic",
      "publishedAt": "2026-09-16T08:00:00.000Z"
    },
    {
      "title": "Web analytics: what it is, how it works and how to use it",
      "url": "https://plausible.io/blog/web-analytics",
      "publishedAt": "2026-09-11T07:00:00.000Z"
    },
    {
      "title": "Web analytics vs product analytics: core difference and how to choose?",
      "url": "https://plausible.io/blog/web-analytics-vs-product-analytics",
      "publishedAt": "2026-09-03T01:00:00.000Z"
    }
  ],
  "personalEmailsOmitted": 0,
  "sourcesChecked": [
    {
      "url": "https://plausible.io/robots.txt",
      "type": "robots",
      "status": 200,
      "ok": true,
      "ms": 292
    },
    {
      "url": "https://plausible.io/",
      "type": "home",
      "status": 200,
      "ok": true,
      "ms": 292
    },
    {
      "url": "https://plausible.io/contact",
      "type": "contact",
      "status": 200,
      "ok": true,
      "ms": 50
    },
    {
      "url": "https://plausible.io/blog/feed.xml",
      "type": "feed",
      "status": 200,
      "ok": true,
      "ms": 63
    },
    {
      "url": "https://plausible.io/privacy",
      "type": "privacy",
      "status": 200,
      "ok": true,
      "ms": 65
    },
    {
      "url": "https://plausible.io/about",
      "type": "about",
      "status": 200,
      "ok": true,
      "ms": 66
    },
    {
      "url": "https://plausible.io/terms",
      "type": "terms",
      "status": 200,
      "ok": true,
      "ms": 23
    },
    {
      "url": "https://plausible.io/sitemap.xml",
      "type": "sitemap",
      "status": 200,
      "ok": true,
      "ms": 77
    },
    {
      "url": "https://plausible.io/imprint",
      "type": "legal",
      "status": 200,
      "ok": true,
      "ms": 80
    },
    {
      "url": "https://plausible.io/jobs/customer-success",
      "type": "careers",
      "status": 200,
      "ok": true,
      "ms": 31
    }
  ],
  "pagesFetched": 9,
  "durationMs": 556,
  "scrapedAt": "2026-11-04T22:27:09.025Z",
  "outputVersion": 1,
  "schemaVersion": "1.1.0",
  "changeType": null,
  "changes": [],
  "pro": null,
  "insight": null
}
```

### Pro mode: who to call first and why

Turn on **Pro mode** (one checkbox) and every company also gets:

| Field | What it tells you |
|---|---|
| `pro.buyingIntentScore` | 0–100, easy to sort: hiring (more for 20+ open roles), a post in the last 30 days, sales/marketing tools (HubSpot, Intercom, Calendly...), selling online, being reachable, and in monitor mode started hiring / new tools / new posts. Every point comes with its reason in `pro.buyingSignals[]`. |
| `pro.estimatedSize` | `1-10`, `11-50`, `51-200`, `201-1000` or `1000+`, with the public evidence used (`"72 open roles"`, `"phones in 5 countries"`). |
| `pro.icpFitScore` | 0–100 = share of **your** ideal-customer criteria that the company meets (`idealCustomer`: countries, languages, technologies to have or not have, hiring, minimum open roles, keywords, sizes, has contact), with `icpMatched` / `icpMissed`. |
| `pro.whyNow` | one line with the concrete reason to reach out today: *"72 open roles on lever."*, *"Started hiring (3 open roles)."* |
| `pro.trend` | in monitor mode, what changed since you started monitoring (up to 90 snapshots per company): open roles +15, tools added, new posts. Nobody else has this history: it grows with every scheduled run. |
| `insight` | one sentence by AI (Claude Haiku) using only the facts in the row, for example *"Blue Bottle Coffee is hiring 72 people on Lever and runs Zendesk — a good moment to pitch support tooling."* If the AI is not available the field is `null` and nothing extra is charged for it. |

Pro mode does not read more pages, so it is just as fast. Price: see below.

### Use cases

- **Enrich a lead or account list**: paste domains from your CRM or a spreadsheet and get emails, phones and LinkedIn per company.
- **Tech-stack prospecting**: find which accounts run Shopify, Magento, HubSpot, Intercom or Tiendanube, and target them.
- **Hiring signals**: companies hiring (and how many open roles) are buying — route them to your SDRs.
- **Personalized outreach**: open with their latest blog post or their new job openings.
- **Account monitoring**: weekly alerts when a target account starts hiring, adds a tool or publishes.

### Schedule it: monitor companies and get only what changed

1. Turn on **Monitor mode** (optionally give it a **Monitor state name**, e.g. `my-accounts-weekly`).
2. Add a Slack webhook or a webhook URL (Zapier, Make, n8n — use it to send emails too) for alerts.
3. Open **Schedules** → **Create schedule** → daily or weekly → select this Actor with your input.

Each run compares every company with its previous profile and outputs only companies that are new or changed (each item has `changeType`: `new` or `changed`, and a `changes` list). You pay only for those. Detected changes: `started_hiring`, `stopped_hiring`, `open_roles_changed`, `technology_added`, `technology_removed`, `new_post`, `contact_added`, `contact_removed`, `social_changed`, `company_info_changed`. Hosting/framework changes are ignored, and a contact is only reported as removed if the page where it was seen was read successfully — no false alarms from a flaky page.

With **Pro mode**, monitored companies also get `pro.trend` (open roles, tools and posts since the first run, up to 90 snapshots) and `buying_intent` signals from the changes.

Example Slack alert: *Tiendanube (tiendanube.com): Started hiring; Added HubSpot; Removed Intercom*.

### How much does it cost to enrich companies?

Pay per result (pay per event): no subscription, no platform usage fees, and failed, blocked or empty sites are free. **One price per company, no matter how many pages are read** (at most `maxPagesPerDomain`, default 10). The log shows the maximum cost of the run before it starts.

| Companies returned | Cost |
|---|---|
| 100 companies | USD 1 |
| 1,000 companies | USD 10 |
| 10,000 companies | USD 100 |

**Pro mode** adds USD 0.02 per company (USD 20 per 1,000) on top of the base price, only for companies returned.

Apify subscribers get a lower price per company (tiers on the Pricing tab). In monitor mode you pay only for new or changed companies. Set **Maximum cost per run** and the Actor stops exactly there.

### How to use the company enrichment scraper

1. Click **Try for free**.
2. Paste your domains in **Domains or website URLs** (e.g. `plausible.io`, `https://www.katzsdelicatessen.com`).
3. Click **Start** and download JSON, CSV or Excel, or get the data through the API.

#### Input example

```json
{
  "domains": ["plausible.io", "katzsdelicatessen.com", "tiendanube.com"],
  "includePersonalEmails": false,
  "maxPagesPerDomain": 10
}
```

#### Input fields

| Field | Description |
|---|---|
| `domains` | Company domains or website URLs (also accepts emails: their domain is used). Bulk: thousands per run. |
| `sections` | Which parts to collect: `contacts`, `social`, `technologies`, `hiring`, `posts` (default: all). Fewer sections = fewer pages read. |
| `includePersonalEmails` | Off by default: only generic/role inboxes; personal-looking emails are omitted and counted in `personalEmailsOmitted`. |
| `maxPagesPerDomain` | Hard cap of pages per company (default 10): home + contact, legal notice/Impressum, privacy, about, team, terms, careers or ATS API, blog/feed, sitemap — in order of how likely they are to have contacts. |
| `maxPosts` | Latest blog posts per company (default 5, 0 disables). |
| `domainTimeoutSecs` | Max time per company (default 15 s); returns what was found when reached. |
| `maxResults` | Stop after this many companies (cost cap). |
| `maxConcurrency` | Companies processed in parallel (default 10). |
| `respectRobotsTxt` | On by default. |
| `monitorMode`, `monitorStateName`, `notifyWebhookUrl`, `notifySlackWebhookUrl` | Monitor mode and alerts (see *Schedule it*). |
| `proMode`, `idealCustomer` | Pro mode extras (see *Pro mode*); `idealCustomer` example: `{ "countries": ["US"], "technologies": ["Shopify"], "mustBeHiring": true, "sizes": ["11-50", "51-200"] }`. |
| `savedListName`, `saveList` | Give your company list a name and the Actor remembers it for the next (scheduled) runs; new domains are added to it. |
| `outputFormat` | `standard` (default) or `leader-compatible` (see *Migrating from other scrapers*). |
| `startUrls`, `maxRequestsPerStartUrl` | Aliases of `domains` and `maxPagesPerDomain`, so the input of other contact scrapers works as is. |
| `proxyConfiguration` | Optional; off by default (company websites are read directly). |

### Integrations and code examples

Bulk input is fine: thousands of domains in one run, 10 in parallel by default, a few requests at most per site, strict time limits and your budget cap.

#### Python

```python
from apify_client import ApifyClient

client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("cleanyield/company-enrichment-from-website").call(
    run_input={"domains": ["plausible.io", "katzsdelicatessen.com"], "sections": ["contacts", "social", "hiring"]}
)
for company in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(company["companyName"], [e["email"] for e in company["emails"]], company["hiring"]["isHiring"])
```

#### JavaScript

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

const client = new ApifyClient({ token: '<YOUR_APIFY_TOKEN>' });
const run = await client.actor('cleanyield/company-enrichment-from-website').call({
    domains: ['plausible.io', 'katzsdelicatessen.com'],
    sections: ['contacts', 'social', 'technologies'],
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
console.log(items.map((c) => [c.companyName, c.phones[0]?.phone, c.technologies.map((t) => t.name)]));
```

#### Sync mode (Clay, n8n, Make, HTTP)

One HTTP call returns the company profile directly (typically 2–6 s for one domain, always under ~20 s thanks to the per-domain time limit):

```
POST https://api.apify.com/v2/acts/cleanyield~company-enrichment-from-website/run-sync-get-dataset-items?token=<YOUR_APIFY_TOKEN>
Content-Type: application/json

{ "domains": ["plausible.io"], "domainTimeoutSecs": 15 }
```

An empty array means the site could not be enriched (and was not charged).

#### n8n, Make and Clay

Ready-made templates in this Actor's repository: **n8n** "Enrich company domains from Google Sheets" and "Weekly buying signals to Slack", **Make** "Enrich new CRM accounts", **Clay** "HTTP API enrichment column". To send results to **Google Sheets** automatically, use the n8n Sheets template (reads domains from a sheet, writes one row per company with email, phone, LinkedIn and hiring) or Make's Google Sheets module after *Run an Actor*. Monitor mode already sends alerts to **Slack** or any **webhook**, and every run saves a visual **dashboard** (record `DASHBOARD`, link in `SUMMARY`) you can open and share. Or use the official Apify node/app → **Run an Actor** → `cleanyield/company-enrichment-from-website`.

### Migrating from other scrapers

Coming from a contact details scraper? Paste the same input and choose the output you need — your integration keeps working.

| Their input | Here | Their output field | Here (`outputFormat: "leader-compatible"`) | Here (standard) |
|---|---|---|---|---|
| `startUrls` `[{ "url": "https://acme.com" }]` | same (or `domains: ["acme.com"]`) | `emails` (strings) | `emails` | `emails[].email` + type, `primaryEmail` |
| `maxRequestsPerStartUrl` | same (= `maxPagesPerDomain`) | `phones` (strings) | `phones` (E.164) | `phones[].phone` + national, country, isWhatsApp |
| `maxDepth`, `sameDomain` | not needed: same site only, best pages picked for you | `linkedIns`, `twitters`, `facebooks`, `instagrams`, `youtubes`, `tiktoks`, `pinterests`, `discords`, `threads`, `telegrams`, `reddits` | same names (lists) | `socialProfiles.linkedin`, `.x`, `.facebook`... |
| `useBrowser` | not needed (HTML first, fast) | `whatsapps` | `whatsapps` (wa.me links) | `phones[].isWhatsApp` |
| `proxyConfig` | `proxyConfiguration` | `url`, `domain`, `originalStartUrl`, `depth` | same | `url`, `domain` |

Differences you will notice: **one row per company** instead of one row per page (no deduplication step), **fixed price per company** instead of per page, phones normalized to E.164, emails classified and validated. With `leader-compatible`, our extra data (technologies, hiring, posts, `emailDetails`, `phoneDetails`) comes in the same row with our own field names.

### For developers: use it inside your own Actor

Building an Actor, an agent tool or a SaaS that needs company data? Call this one and focus on your product:

- **Fast and synchronous**: `run-sync-get-dataset-items` answers in seconds per domain, with a hard time limit per domain.
- **Stable schema**: fields are only added, never renamed; every item has `schemaVersion` ([CHANGELOG](CHANGELOG.md)).
- **Clear price**: a fixed price per valid company, nothing for failures; your users' runs never get surprise bills.

```js
import { Actor } from 'apify';
import { ApifyClient } from 'apify-client';

const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('cleanyield/company-enrichment-from-website').call(
    { domains: ['acme.com'], sections: ['contacts', 'social'] },
    { waitSecs: 60 },
);
const { items: [company] } = await client.dataset(run.defaultDatasetId).listItems();
await Actor.pushData({ domain: 'acme.com', email: company?.primaryEmail ?? null, phone: company?.primaryPhone ?? null });
```

### Measured quality

Measured on a **fixed test set of 65 real domains** (SaaS, ecommerce, restaurants and small businesses, LATAM, US and Europe, plus a domain that does not exist), checked against the raw HTML of each site. Last run: 2026-11-04, version 1.1.0.

| Metric | Result |
|---|---|
| Successful runs | **100%** (65/65) |
| Complete results (all required fields) | **100%** |
| Key fields correct (426 of 428 checked by hand) | **99.5%** |
| Companies with at least one email / phone | **86% / 58%** |
| Time per company (median / p90 / max) | **1.8 s / 3.1 s / 5.9 s** |

Every version is measured on the same set before it is published. What changed and when: [CHANGELOG](CHANGELOG.md).

### Use with AI agents / MCP

This Actor charges only per result (no usage fees), so AI agents can run and pay for it through the [Apify MCP server](https://mcp.apify.com) without an Apify account.

Example request to your assistant:

> "For these 20 domains, tell me which companies are hiring, which ATS they use, their tech stack and a generic contact email."

> "Find a contact email and phone for bakery-sonnenschein.de, acme.com.ar and joespizzanyc.com and give me a table."

> "Which of these 50 accounts should my SDRs call first this week? Use Pro mode and sort by buying intent."

For agents: input `domains` (list of domains or URLs), optional `sections` to read fewer pages, `proMode: true` for scores. One output item per company; read `primaryEmail`, `primaryPhone`, `socialProfiles`, `hiring.isHiring`, `technologies[].name`, and in Pro mode `pro.buyingIntentScore` and `pro.whyNow`. Companies that could not be read are not returned and not charged.

### Related Actors

- [ATS Jobs Scraper: Greenhouse, Lever & Ashby Job Listings](https://apify.com/cleanyield/ats-jobs-scraper) — found a company that is hiring? Get every open job with salary, location and description.
- [FMCSA Carrier Data: US Trucking Company Directory](https://apify.com/cleanyield/fmcsa-carrier-data) — build a list of US trucking companies, then enrich their websites here.

### FAQ

#### Is it legal to scrape company websites?

This Actor reads only public pages of the company's own website, without login, respects robots.txt and never bypasses bot challenges. Company contact data (info@, sales@, the main phone) is generally low risk. Emails that identify a person (jane@company.com) are personal data under GDPR, so they are **omitted by default**; if you turn on `includePersonalEmails`, you are responsible for having a legal basis (e.g. legitimate interest) and for informing those people where required (GDPR art. 14). Nothing is stored outside your own Apify storage.

#### What happens if a website fails or comes back incomplete?

It is not charged. Down, parked, blocked (403, Cloudflare challenge) and empty sites are listed with the reason in `FAILED_DOMAINS`, and every run stores a `RUN_STATS` record (successes, failures by reason) in its key-value store. One bad domain never fails the whole run.

#### How many pages does it read per website?

At most `maxPagesPerDomain` (default 10): the home page, then the pages most likely to have contact data — contact, legal notice/Impressum, privacy policy, about, team and terms, in English, Spanish, Portuguese, German, French and Italian — plus careers (or the ATS public job API), blog or feed and the sitemap, found through same-site links and common paths. Requests to the same site go a few at a time. robots.txt does not count. That is why runs are fast and the price is fixed.

#### Why is a site "blocked"?

Some sites (often large brands behind Cloudflare) show a bot challenge or refuse automated requests. We report them as `blocked` and do not try to get around it. In our tests about 14% of large-brand sites and very few small-business sites do this.

#### Does it work for JavaScript-heavy sites?

It reads the HTML the server sends (fast and cheap, no browser). Most company sites include contacts, links, structured data and scripts in that HTML; content that only appears after JavaScript runs may be missed.

#### Will the output format change?

No. The output schema is stable and versioned (`schemaVersion: "1.1.0"`, `outputVersion: 1`): fields are only added, never renamed or removed, and every change is in the [CHANGELOG](CHANGELOG.md).

#### Why do I see fewer emails than another scraper?

We return one deduplicated row per company and drop what is not a real company inbox: placeholders (`your@email.com`), image names (`logo@2x.png`), package versions, regulators and agencies found in legal texts. Personal emails (jane@company.com) are omitted by default for GDPR and counted in `personalEmailsOmitted`; turn on `includePersonalEmails` to get them.

#### How do I know what a run will cost?

The log prints the maximum cost before the run starts (number of companies × price, plus Pro mode if on), and **Maximum cost per run** stops it exactly there. Failed, blocked and empty sites are free.

#### Is this an official tool of any of the platforms it detects?

No. Not affiliated with or endorsed by LinkedIn, X, Facebook, Instagram, Greenhouse, Lever, Ashby, Shopify, HubSpot or any other brand named on this page; their names are used only to describe the data found on public company websites.

#### I found a bug / need a feature

Open an issue in the **Issues** tab. We usually answer issues within 24 hours.

# Changelog

This Actor's version history is a separate document: https://apify.com/cleanyield/company-enrichment-from-website/changelog.md

# Actor input Schema

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

Company domains or website URLs, one per line. Accepts 'acme.com', 'www.acme.com', 'https://acme.com/about' or even an email like 'info@acme.com' (its domain is used). Duplicates are removed. Bulk is fine: thousands per run. Example: plausible.io Also accepts "startUrls" or "urls" (same format as other contact scrapers).

## `sections` (type: `array`):

Which parts of the company profile to collect. Fewer sections = fewer pages read and faster runs. companyName, description, language, country and address are always included. Options: contacts (emails and phones), social (LinkedIn, X, Facebook, Instagram, YouTube, TikTok, GitHub), technologies (tech stack), hiring (careers page, ATS, open roles), posts (latest blog posts). Default: all. Example: \["contacts", "social"]

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

Off by default (GDPR-friendly): only generic and role inboxes are returned (info@, hello@, sales@, ventas@, support@...). Emails that look like a person's (jane@, j.smith@) are omitted and only counted in personalEmailsOmitted. Turn on to include them, each marked with type 'personal'. If you turn it on, you are responsible for having a legal basis to process them.

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

Hard cap of pages read per company: the home page plus the pages most likely to have contact data (contact, legal notice/Impressum, privacy policy, about, team, terms) and the careers, blog/feed and sitemap pages, all found through same-site links and common paths in English, Spanish, Portuguese, German, French and Italian. Default 10 (median 1.8 s per company). The price per company does not change with this value.

## `maxPosts` (type: `integer`):

How many of the latest blog posts (title, URL, ISO date) to return per company, from its RSS/Atom feed or blog page. 0 disables posts. Example: 5.

## `domainTimeoutSecs` (type: `integer`):

Maximum time spent on one company. When reached, the Actor returns what it already found (if valid). Keeps sync calls from Clay, n8n or Make under ~20 s. Example: 15.

## `maxResults` (type: `integer`):

Stop after this many companies are returned. Use it to cap cost on big lists; you only pay for companies actually returned. Leave empty for no limit. Example: 1000.

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

How many companies are processed at the same time (each one on its own host, with at most a few requests in parallel per site). Example: 10.

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

On by default: pages disallowed by the site's robots.txt for all crawlers are not read, and sites that disallow everything are reported as robots_disallowed (not charged).

## `proMode` (type: `boolean`):

One checkbox for all the extras: buying-intent score 0–100 with the signals behind it (hiring, open roles, recent posts, sales/marketing tools, and in monitor mode new tools, new posts or started hiring), estimated company size, ICP fit score against your ideal customer, a 'why now' line for sales, the trend since you started monitoring, and a one-sentence AI insight per company. Charged as an extra event per company (see Pricing). Off by default.

## `idealCustomer` (type: `object`):

Optional, used in Pro mode to compute icpFitScore (0–100 = share of your criteria that the company meets). Keys (all optional): countries (ISO-2 list), languages (ISO 639-1 list), technologies (any of these, e.g. \["Shopify","HubSpot"]), excludeTechnologies, mustBeHiring, minOpenRoles, keywords (in name or description), sizes (\["11-50","51-200"]), mustHaveContact.

## `monitorMode` (type: `boolean`):

For scheduled runs: remember each company's last profile and output only companies that are new or changed since the previous run, with a list of changes (started hiring, open roles changed, technology added/removed, new blog post, contact added/removed, social profile changed). You pay only for new/changed companies. Schedule the Actor daily or weekly from the Schedules tab.

## `monitorStateName` (type: `string`):

Optional name of the storage that keeps the previous profiles. Use the same name across scheduled runs to compare against them. Leave empty to derive it automatically from the domain list and sections. Lowercase letters, numbers and hyphens only. Example: my-accounts-weekly.

## `notifyWebhookUrl` (type: `string`):

Optional. When monitor mode finds changes, POST a JSON summary (companies and their changes) to this URL (works with Zapier, Make, n8n). Example: https://hooks.zapier.com/hooks/catch/123/abc

## `notifySlackWebhookUrl` (type: `string`):

Optional. Slack incoming-webhook URL to receive a message when monitor mode finds changes, e.g. 'Acme (acme.com): Started hiring (12 open roles); Added HubSpot'. Example: https://hooks.slack.com/services/T000/B000/XXXX

## `savedListName` (type: `string`):

Optional. Give your list of companies a name and the Actor remembers it: the next runs (for example scheduled ones) enrich the saved list plus any new domains you add, without pasting it again.

## `saveList` (type: `boolean`):

When a saved list name is set, add the domains of this run to it (on by default).

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

Optional. By default company websites are read directly (no proxy), which works for almost all sites and keeps cost low. Sites that block automated requests are reported as blocked and not charged; the Actor never tries to bypass bot challenges. Example: {"useApifyProxy": false}

## `outputFormat` (type: `string`):

'standard' (default): our clean company profile. 'leader-compatible': the field names popular contact scrapers use (emails and phones as string lists, linkedIns, twitters, facebooks, instagrams, youtubes, tiktoks, pinterests, whatsapps...), one row per company, with our extra fields next to them (emailDetails, phoneDetails, technologies, hiring...). Use it to switch without changing your integration.

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

Same as "domains", accepted so you can reuse the input of other contact scrapers (\[{"url": "https://acme.com"}]). Each start URL is one company.

## `maxRequestsPerStartUrl` (type: `integer`):

Alias of maxPagesPerDomain, accepted for compatibility with other contact scrapers.

## Actor input object example

```json
{
  "domains": [
    "plausible.io",
    "katzsdelicatessen.com",
    "tiendanube.com"
  ],
  "sections": [
    "contacts",
    "social"
  ],
  "includePersonalEmails": false,
  "maxPagesPerDomain": 10,
  "maxPosts": 5,
  "domainTimeoutSecs": 15,
  "maxResults": 1000,
  "maxConcurrency": 10,
  "respectRobotsTxt": true,
  "proMode": false,
  "idealCustomer": {
    "countries": [
      "US",
      "AR"
    ],
    "technologies": [
      "Shopify"
    ],
    "mustBeHiring": true,
    "sizes": [
      "11-50",
      "51-200"
    ],
    "mustHaveContact": true
  },
  "monitorMode": false,
  "monitorStateName": "my-accounts-weekly",
  "notifyWebhookUrl": "https://hooks.zapier.com/hooks/catch/123/abc",
  "savedListName": "my-target-accounts",
  "saveList": true,
  "proxyConfiguration": {
    "useApifyProxy": false
  },
  "outputFormat": "standard",
  "startUrls": [
    {
      "url": "https://plausible.io"
    }
  ],
  "maxRequestsPerStartUrl": 10
}
```

# Actor output Schema

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

All valid results of this run (only these are charged). JSON, CSV or Excel from the dataset.

## `runStats` (type: `string`):

RUN_STATS: results saved, failures by reason (not charged), retries and success rate.

## `dashboard` (type: `string`):

DASHBOARD: a shareable HTML page with charts and rankings for this run.

# 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": [
        "plausible.io",
        "katzsdelicatessen.com",
        "tiendanube.com"
    ],
    "proxyConfiguration": {
        "useApifyProxy": false
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("cleanyield/company-enrichment-from-website").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": [
        "plausible.io",
        "katzsdelicatessen.com",
        "tiendanube.com",
    ],
    "proxyConfiguration": { "useApifyProxy": False },
}

# Run the Actor and wait for it to finish
run = client.actor("cleanyield/company-enrichment-from-website").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": [
    "plausible.io",
    "katzsdelicatessen.com",
    "tiendanube.com"
  ],
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}' |
apify call cleanyield/company-enrichment-from-website --silent --output-dataset

```

## MCP server setup

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

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/AGvOynIFZwM4GeTuo/builds/jZjoexO0DGRZJwpqS/openapi.json
