# Firmographics — Company Website Enrichment (`tidytools/company-website-enrichment`) Actor

Firmographics from each company's own website: industry, business model, stated size clues and contact details. Enter company names, domains or Google Maps leads; rows map back to the source. $6/1,000.

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

## Pricing

from $4.80 / 1,000 enriched 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

### What does Firmographics — Company Website Enrichment do?

Turn company domains, names or a Google Maps lead dataset into website-sourced firmographics and contacts. For example, `stripe.com` becomes one row with industry, business model, a company-size band when stated on its website, contact details and tech stack. Data comes from each company's own website, not LinkedIn headcounts or revenue estimates:

- 🏢 **Name** and a **one-line description** of what the company does, written by AI from the website text
- 🏷️ **Industry** (31 categories, e.g. `software-saas`, `restaurants-food-service`, `legal-services`) and **business model** (`B2B`, `B2C`, `B2B2C`, `nonprofit`, `government`), with a confidence and a short reason
- 👥 **Company size** (employee band such as `11-50` or `1001-5000`) when the company states it: its own schema.org `numberOfEmployees`, or a team size written on the website; otherwise empty, never guessed
- 📧 **Emails, phone numbers and social profiles** (Facebook, X, LinkedIn, Instagram, YouTube, TikTok and more; company pages only, a person's LinkedIn profile is not returned), with obfuscated and Cloudflare-protected emails decoded and junk filtered out
- 📍 **Postal address and logo** from the site's schema.org data
- 🌐 **Language** and a **country guess** (from the postal address, else the country of its phone numbers, else the domain ending such as `.de`), with the source of the guess
- 🧰 **Tech stack**: CMS, e-commerce platform, analytics, tag manager, CDN, marketing tools and more, detected from the home page (included in the price; can be turned off)

For each company it reads the home page plus up to 2 likely contact or about pages (configurable). Paste the home page or any page of the site: if a deep link you pasted is gone (404), the home page is read instead. Business directory pages (411.ca, Yellow Pages, Yelp), social pages, parked domains, sign-in pages and unreachable sites are not read as companies and cost nothing (robots.txt does not stop a site you list unless you turn on `respectRobotsTxt`).

#### Company names instead of domains

Put names in **Company names** (or in the domain list), e.g. `Basecamp` or `Hofbräuhaus München`, optionally with **Country of the company names** (`DE` tries `.de` first). For each name the likely domains are checked (basecamp.com, basecamp.io…; for a longer name also its leading words, e.g. stumptowncoffee.com for "Stumptown Coffee Roasters"), and a domain is used only when its home page names itself as that company (schema.org, `og:site_name` or the page title) and is not a parked page. The row shows how it was found (`websiteFromName`: the name, the domain and the site name it matched). Names without a verified website get a free row (`errorType: "company_not_found"`, with the domains tried). The company is then enriched and charged like any other ($0.006, only with an AI result); the lookup itself costs nothing. In our check of 20 company names (October 10, 2026) 15 were found, all correct; the others got a free row. Small local businesses often use domains that cannot be guessed, so give their website or a Google Maps dataset where you can.

### Use cases

- **Lead lists**: turn bare domains into segmentable records (industry, B2B/B2C, country) with contact details
- **Enrich Google Maps or directory results**: pass the run's dataset and its `website` field
- **Sales lead lists**: run it on the dataset of Website Email Extractor for Google Maps Leads & Domain Lists or Email List Cleaner - Bulk Email Validator & Disposable Check; every lead row keeps its email and send status and gets the company's industry, size and social profiles (see [Lead-cleaning workflow](#lead-cleaning-workflow-google-maps-to-enriched-sales-leads))
- **Market research**: see which industries and technologies appear in a list of companies
- **CRM hygiene**: fill in missing descriptions, socials and countries

### How much does it cost?

| Event | Price |
|---|---|
| Enriched company | **$6.00 / 1,000 companies** ($0.006 each, AI included) |

**Example:** 250 rows from a Google Maps run cost at most 250 × $0.006 = **$1.50** on the Free plan; unreachable websites are free.

**No start fee.** One price per company, however many pages are read. **Unreachable websites are not charged**; they carry an `errorType`. If the AI part fails (for example a website with almost no text), the contact details are still returned with an `aiError` and the company is **not charged**. Higher Apify plans get volume discounts (see the *Pricing* tab).

#### Control your cost

- **Before anything is read**, the run logs its plan: the number of companies × the price = the most the run can cost, compared with your maximum charge per run.
- **Charged**: a company whose website was read and classified by the AI ($0.006). Every row has `charged: true` or `false`.
- **Free**: unreachable websites, parked or for-sale domains, "coming soon" and default server pages (`success: false`, `siteStatus` `parked` / `placeholder` / `default_server_page`, no AI call), websites without an AI result (`aiError`), sites that redirect to a sign-in page (`errorType: "login_required"`), sites that are a sign-in page or answer HTTP 401, sites whose robots.txt disallows the home page when `respectRobotsTxt` is on (`robots_disallowed`), social and business directory pages given as the website (`not_a_website`), company names whose website could not be verified (`company_not_found`), other lines that are not a website, and duplicates (`acme.com` and `www.acme.com` are one company). More pages, the browser fallback and our own network cost nothing extra; only an Apify Proxy you choose yourself is billed by Apify.
- **Maximum charge per run**: set it in the run options (Console) or with `maxTotalChargeUsd` (API). When the next company could exceed it, no new company is started; the run ends with status `LIMIT_REACHED`, and the `SUMMARY` record lists the companies not processed (`notProcessed`: the count and up to 100 inputs) so you can run them again with a higher limit.
- **If Apify restarts the run** (server migration or Resurrect), items already finished are skipped and not charged again (`SUMMARY.resumedSkipped`).

#### Price comparison (checked October 2026)

| Actor | Price per company |
|---|---|
| **Lead & Company Enrichment (this Actor)** | **$0.006**, AI description, industry, contacts and tech stack included |
| foxlabs/company-enrichment | $0.001 per company record |
| vdrmota/contact-info-scraper | $0.002 per crawled page; with its default crawl settings about $17-35 per 1,000 companies; contacts only |
| Google Maps scraper contact add-on (compass/crawler-google-places) | $0.002 per place; only while scraping Maps again, cannot enrich an existing dataset |
| renzomacar and george.the.developer company enrichment Actors | $0.02 per company |

foxlabs/company-enrichment is cheaper per record; compare its fields with what you need. This Actor reads each company's own website live and returns where every email and phone number was found.

### Switching from another company enrichment Actor

The two most used domain-to-company Actors on the Store, side by side (prices from the Apify Store, checked October 2, 2026; Free plan prices, higher plans can be lower):

| | **This Actor** | foxlabs/company-enrichment | renzomacar/company-enrichment |
|---|---|---|---|
| Price per 1,000 companies | **$6** | $1 | $20 |
| Start fee per run | **none** | $0.00005 per GB of memory | $0.01 |
| Failed lookups charged? | **No** (unreachable, parked, no AI result, invalid lines, duplicates) | No (since September 25, 2026) | No (below your `minConfidence`) |
| Data source | the company's own website, read live | public Owler company profiles | the company's own website, read live |
| Emails, phones, social profiles | yes, with the page each was found on | no | yes, plus decision-makers |
| Description and industry | AI description, industry and B2B/B2C | from the Owler profile | description, industry with NAICS/SIC codes |
| Employees, revenue band, founded year | employee band only when the company states it | yes | estimated, when published |
| Tech stack | yes, included | no | yes |
| Accepts company names without a domain | yes, when the website names itself as that company (else a free `company_not_found` row) | yes | no |
| Google Maps dataset in, rows mapped back 1:1 | yes (`datasetId`, `datasetKeepFields`) | no | no |
| Email extractor / email validator dataset in, lead columns kept | yes (one charge per company) | no | no |

Choose foxlabs/company-enrichment when you need firmographics such as revenue, or a headcount for every company; choose this Actor when you need contacts, a description and the tech stack read from the website, or need to enrich a Google Maps run.

**Paste your existing input.** The field names of those Actors are accepted, so a saved input or API call keeps working; the run log says how each field was read:

```json
{ "companyDomains": ["stripe.com", "notion.so"], "maxResults": 100 }
```

```json
{ "domains": ["stripe.com", "notion.so"], "maxPagesPerDomain": 5 }
```

| Their field | Read here as |
|---|---|
| `companyDomains`, `companies`, `domain` (one string) | `domains` |
| `companyUrls`, `urls`, `startUrls` (`[{ "url": ... }]`) | website URLs (Owler profile URLs are skipped: give the company's website) |
| `maxPagesPerDomain`, `maxRequestsPerStartUrl` | `maxPagesPerSite` (1-10; the price per company is the same) |
| `maxResults`, `maxItems` | the most companies processed in the run (0 = no limit); the rest are listed in `SUMMARY.overMaxResults` and not charged |
| `proxyConfig` | `proxyConfiguration` |
| `companyNames` | each name is looked up to its website (see [Company names instead of domains](#company-names-instead-of-domains)); names without a verified website get a free row |
| `minConfidence`, `brief`, `includeUnmatched`, `maxRequestsPerCrawl` | ignored; every company costs the same, and failed ones are free anyway |

### How to use it

1. Paste domains into **Company domains** (one per line), or pick a **Dataset ID** from another run and set **Dataset field** (e.g. `website`). Blank lines are ignored; company names are looked up to their website (see [Company names instead of domains](#company-names-instead-of-domains)); a line that is neither, such as a gmail.com address, gets one free `invalid_input` row.
2. Click **Start** and export the results as JSON, CSV or Excel.

#### Input example

This is the input the form is prefilled with; a run with it takes about a minute and costs only cents:

```json
{
    "domains": [
        "basecamp.com",
        "hofbraeuhaus.de"
    ]
}
```

#### Output example (real result, September 2026)

```json
{
    "input": "dallmayr.com",
    "url": "https://dallmayr.com",
    "domain": "dallmayr.com",
    "success": true,
    "name": "Dallmayr",
    "description": "Dallmayr is a company that sells high-quality food and beverage products, including coffee, tea, and delicatessen items, to consumers through its retail stores, online shop, and vending services.",
    "industry": "consumer-products",
    "businessModel": "B2C",
    "industryConfidence": "high",
    "primaryEmail": "info@dallmayr.de",
    "primaryPhone": "+498921350",
    "emails": ["info@dallmayr.de"],
    "phones": ["+498921350", "+49892135130"],
    "socialProfiles": {
        "facebook": "https://www.facebook.com/dallmayr",
        "instagram": "https://www.instagram.com/dallmayr_de",
        "tiktok": "https://www.tiktok.com/@dallmayr_de",
        "linkedin": "https://de.linkedin.com/company/dallmayr-ohg",
        "youtube": "https://www.youtube.com/user/dallmayrkaffee"
    },
    "address": null,
    "language": "en",
    "countryGuess": "DE",
    "countrySource": "phone",
    "techStack": {
        "CMS": ["TYPO3"],
        "Analytics": ["Google Analytics"],
        "Advertising": ["Google Ads"],
        "CDN": ["Cloudflare"],
        "Programming language": ["PHP"],
        "Cookie consent": ["Cookie Consent (Osano open source)"]
    },
    "via": "direct",
    "charged": true
}
```

Other results from the same test of 10 companies: basecamp.com → `software-saas` / `B2B`; stripe.com → `financial-services` / `B2B`; kanzlei-hoenig.de → `legal-services`, country `DE`; joespizzanyc.com → `restaurants-food-service`, 10 branch phone numbers, Squarespace; stumptowncoffee.com → `consumer-products`, address "700 SW 5th Ave, 3rd Floor, #400, 97214 Portland, OR", Shopify.

`countryGuess` is a guess that weighs several signals (postal address, ccTLD, international phone numbers on the home, contact and imprint pages, and, only as confirmation, hreflang / og:locale / page language). It says where it came from (`countrySource`: `address`, `tld`, `phone` or `locale`), how sure it is (`countryConfidence`: `high`, `medium`, `low`) and lists the evidence in `countrySignals`. Multinational sites list branch numbers from many countries, so numbers on deep pages (team, support, legal) do not decide the country and are only returned when they match it. With no usable evidence (for example a global `.com` site) there is no guess (`countryGuess: null`, `countrySource: "none"`). A support page that lists numbers for a dozen countries gives no guess either, and when the site moved to a generic domain (toyota.co.jp to global.toyota), the ending of the address you gave counts. A website with (almost) no readable text, such as a JavaScript shell or a hosted-platform page, is read once more in a browser; if there is still nothing to describe, the record has `thinContent: true` and an `aiError`, and is not charged. `primaryEmail` (an email on the company's own domain, a role mailbox such as info@, contact@, hello@ or sales@ before a person's address) and `primaryPhone` are single fields for spreadsheets and CRMs. Each record also has `emailDetails` and `phoneDetails` (page and method per value), `technologies` (with confidence) and `pagesChecked`. A `SUMMARY` record in the key-value store has the run `status` (`SUCCESS`, `PARTIAL_RESULTS`, `FAILED`, `NO_RESULTS`, `LIMIT_REACHED`) and a count per industry.

#### Keep the fields of the source dataset

With a **Dataset ID**, set **Dataset fields to keep** (e.g. `title`, `placeId`, `address`) and every output row carries a `source` object with those fields plus `datasetItemIndex` (the item's position in the dataset), so a Google Maps place maps back to its enriched company. Each row also has `input` (the original line or dataset value) and, when several inputs were the same website, `inputs`.

```json
{ "datasetId": "YOUR_DATASET_ID", "datasetField": "website", "datasetKeepFields": ["title", "placeId"] }
```

Every item of the dataset gets exactly one output row, so a Google Maps export joins back 1:1 (by `datasetItemIndex` or a kept field such as `placeId`):

- A place **without a website** gets a row with `errorType: "no_website"` (not charged).
- A place whose "website" is an **Instagram, Facebook, Yelp, Linktree, 411.ca, Yellow Pages or similar page** is not read as a company website (that would return the platform's own details): its row has `errorType: "not_a_website"` and is not charged.
- **Branches of a chain** that share one website are read and charged once; each other branch gets a copy of the result with its own `source`, `charged: false` and `duplicateOf`.
- A **deep link that is gone** (an old `/en/contact` URL, HTTP 404) does not fail the company: the home page is read instead and the row has a `note`. A site that **redirects to a sign-in page** (a private Google Sites page, for example), is only a password form or answers HTTP 401 gets `errorType: "login_required"`, is not charged and is never retried with credentials.
- When a website **redirects to a different domain** (for example an expired domain now parked on a directory), the row has `redirectedTo` and a `warning`: the details found may belong to that other site.

**Real example (September 30, 2026).** 30 Google Maps places from compass/crawler-google-places (10 coffee shops in Portland, 10 dentists in Austin, 10 bakeries in Munich), with `datasetKeepFields: ["title", "placeId"]`: 30 of 30 places got a row; 24 companies were enriched and charged (24 × $0.006 = **$0.144**, about **$4.80 per 1,000 places**, run time 31 seconds); the place without a website, the 3 Instagram/Facebook pages and 2 dead domains were free. Descriptions were specific, e.g. *"Brezelina is a bakery chain with multiple locations in Munich…"*. For local businesses, Google Maps' own category is usually the better industry label; the value here is the one-line description, the email, the social profiles and the tech stack.

**This Actor already includes contact details and the tech stack.** For a Google Maps list you do not need to run Website Email Extractor for Google Maps Leads & Domain Lists and Website Tech Stack Detector as well: use this Actor for everything, or only Website Email Extractor for Google Maps Leads & Domain Lists ($0.002 per website) when emails and socials are all you need.

### Lead-cleaning workflow: Google Maps to enriched sales leads

The last step of TidyTools' lead-cleaning chain, for sales teams who want a list they can segment and import: Google Maps results → [Website Email Extractor for Google Maps Leads & Domain Lists](https://apify.com/tidytools/website-contact-extractor) (the emails on each business's own website) → [Email List Cleaner - Bulk Email Validator & Disposable Check](https://apify.com/tidytools/bulk-email-validator) (a verdict and a ranked send list) → this Actor (what the company does, industry, B2B/B2C, size when stated, company social profiles).

1. Run the extractor and the validator as their pages describe (the extractor's **Integrations** tab can start the validator).
2. In the validator's (or the extractor's) **Integrations** tab choose **Connect Actor or task**, pick this Actor and use this input (`{{resource.defaultDatasetId}}` becomes the dataset of the run that just finished):

```json
{ "datasetId": "{{resource.defaultDatasetId}}" }
```

3. Open the run's **Sales leads** view, or export it as CSV or Excel.

**Customer-owned list example (20 businesses; illustrative, not a test run).** A customer with the right to use a 20-row CSV (`row_number,Business,Website,Email`) uploads it as an Apify Dataset. `Website` is each business's own site. TidyTools does not supply the list or collect Google Maps results for you. To add company data to the original rows, use:

```json
{ "datasetId": "CUSTOMER_DATASET_ID", "datasetField": "Website", "findWebsitesForNames": false, "respectRobotsTxt": true }
```

To also fill missing emails, run Website Email Extractor on that same customer Dataset with `{ "datasetId": "CUSTOMER_DATASET_ID", "datasetField": "Website", "rowIdField": "row_number", "onlyMissingEmail": true, "findWebsitesForNames": false, "requiredContact": "email", "respectRobotsTxt": true }`. Optionally send the extractor's Dataset to Email List Cleaner using `{ "datasetId": "CONTACT_RUN_DATASET_ID" }`; the validator checks addresses rather than websites and has no `respectRobotsTxt` setting. Both website-reading inputs explicitly enable robots checks. A row with no usable website stays free (`errorType: "no_website"`, with name lookup off); a robots.txt-disallowed home page is not read and yields a free `errorType: "robots_disallowed"` row. The contact step charges $0.002 only for a website with an email; this Actor charges $0.006 only for a successfully enriched company. At current Free-plan list prices, the **maximum for both website steps** is 20 × ($0.002 + $0.006) = **$0.16**. For 250 distinct businesses, 250 × ($0.002 + $0.006) = **$2.00**, plus at most $0.0005 × the unique email addresses actually checked by the validator. These are ceilings, not success-rate estimates; skipped, failed and duplicate websites can lower the charge. This is an optional recipe: `respectRobotsTxt` still defaults to `false` for both Actors.

What happens with such a dataset (recognised automatically; a Google Maps scraper's own dataset works the same way):

- **The business website is read**: on Google Maps items the `website` field, never the Maps link in `url`. A validated address without a website uses its company domain (`sales@acme.de` → `acme.de`, flagged `websiteFromEmail: true`); Gmail, Outlook and other webmail domains are never used as a website, so such a row gets `errorType: "no_website"` and is not charged.
- **One company, one charge.** Rows with the same website (several addresses of one business, branches of a chain) are read and charged once; every other row gets a copy of the result with `charged: false` and `duplicateOf`.
- **Every row keeps its own lead columns**, flat for CSV and CRM imports, followed by the enrichment fields: from the validator `businessName`, `placeId`, `website`, `email`, `verdict`, `score`, `sendStatus`, `sendPriority`, `isBestEmail`, `rowId`; from the extractor or a Maps dataset `businessName`, `placeId`, `placeAddress`, `placePhone`, `placeCategory`, `mapsUrl`, `website` (and `email`, `bestEmail`).
- **Dataset fields to keep** still adds the `source` object, and **Dataset field** still chooses the website field when you set it.

Example row (real result, 5 October 2026, shortened):

```json
{
    "businessName": "Franklin Barbecue",
    "website": "https://franklinbbq.com/",
    "email": "hello@franklinbbq.com",
    "verdict": "valid",
    "sendStatus": "deliverable",
    "sendPriority": 1,
    "isBestEmail": true,
    "domain": "franklinbbq.com",
    "success": true,
    "name": "Franklin Barbecue",
    "description": "Franklin Barbecue is a restaurant serving barbecue in Austin, Texas, with a focus on high-quality, naturally raised meats.",
    "industry": "restaurants-food-service",
    "businessModel": "B2C",
    "companySize": null,
    "primaryPhone": "+15126531187",
    "socialProfiles": {
        "facebook": "https://www.facebook.com/franklinbbq",
        "instagram": "https://www.instagram.com/franklinbbq",
        "youtube": "https://youtube.com/bbqwithfranklin"
    },
    "address": "900 East 11th Street Austin, TX, 78702 United States",
    "countryGuess": "US",
    "charged": true
}
```

**Real numbers (our test, 5 October 2026):** the validator's 35 rows from the 20-website chain test on the extractor's and validator's pages (small food businesses in the US and Munich). All 35 rows came back; 18 companies were read and charged (18 × $0.006 = **$0.108**); the other 17 rows were further addresses of the same companies (one bakery chain alone had 10) and were free. Every company got a description, an industry (15 `restaurants-food-service`, 2 `consumer-products`, 1 `ecommerce-retail`) and `B2C`; 32 of 35 rows have company social profiles. None of these bakeries publishes a headcount, so `companySize` is empty for all of them; on larger companies it came from their own schema.org data (GitLab: 2,500 employees → `1001-5000`). The whole chain for 20 websites costs about $0.04 + $0.0175 + $0.108 ≈ **$0.17**.

**At the end of every run** the log and `SUMMARY.nextSteps` give the exact Email List Cleaner input for this run's dataset (`"datasetField": "emails"`) and the most it can cost, ready to paste; skipped when the input already came from Email List Cleaner.

### Use with AI agents (MCP)

Connect Apify's MCP server (https://mcp.apify.com?tools=tidytools/company-website-enrichment) to Claude, Cursor or any MCP client, then ask e.g. "What do these 20 companies do, which industry are they in, and how do I contact them?"

```json
{ "domains": ["basecamp.com", "hofbraeuhaus.de"] }
```

Unreachable websites and websites without an AI result are not charged; failed rows carry an `errorType`.

### Use it from code

```bash
curl -X POST "https://api.apify.com/v2/acts/tidytools~company-website-enrichment/run-sync-get-dataset-items?token=YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"domains":["basecamp.com"]}'
```

Send websites in `domains` (plain strings; bare domains work). The URL-list field `startUrls` is also accepted, but Apify rejects the whole run (HTTP 400) when it holds a bare domain or a blank line.

Only need emails, phones and socials? **Website Email Extractor for Google Maps Leads & Domain Lists** by tidytools does that for $2 per 1,000 websites.

**Schedules and integrations:** run it daily or weekly with Apify Schedules, get a webhook when a run finishes, or send the results to Zapier, Make, n8n, Google Sheets, Slack and other apps with Apify integrations. Results can be exported as JSON, CSV, Excel or XML.

### Advanced settings

- **Plain HTTP requests from**: *Auto* (recommended) reads pages from Apify's network first (the raw HTML finds hidden emails and the tech stack), then from our servers, then with a real browser. Every request names us (user agent `TidyToolsContacts/1.0 (+https://apify.com/tidytools)` from Apify's network, `TidyToolsSmartFetch` from our servers and in the browser), and robots.txt is not used to refuse a website you asked for.
- **Skip pages robots.txt disallows** (`respectRobotsTxt`, default off): turn it on to follow robots.txt: disallowed pages are then not read and are listed in `robotsDisallowedPages`, and a website whose home page is disallowed returns `errorType: "robots_disallowed"` (not charged).
- **Time limit per website** (default 120 seconds): after it, no further pages of that website are started and the pages read so far are used. If even the home page could not be read in time, the company fails with `errorType: "timeout"` (not charged).
- **Proxy**: optional Apify Proxy for requests sent from Apify's network (billed to your Apify account).

### Limitations

- Everything comes from the company's public website; no LinkedIn, registry or employee data. `companySize` is filled only when the company states it (its schema.org `numberOfEmployees` or a team size in the website text), so it is empty for most small businesses.
- The description and industry are AI judgments from the first few thousand characters of the home and about pages; very thin or JavaScript-only sites give weaker results.
- The tech stack is detected from the home page's raw HTML; clues in the response headers (server, CDN) are used only when the page was read from Apify's network.
- Phone numbers without a country code are read with the site's likely country, which can be wrong for multi-country sites.

### Responsible use

- The Actor reads only information that a company publishes on its own public website (business emails, phone numbers, social profiles, address). It does not guess, generate or verify personal email addresses, and it does not log in anywhere.
- Every request identifies itself as TidyTools, never as a browser. robots.txt is not used to refuse a website you asked for, unless you turn on **Skip pages robots.txt disallows** (`respectRobotsTxt`). Pages behind a login are reported as `login_required` and never logged into, bot checks and CAPTCHAs are never solved, and when a site answers HTTP 429 with Retry-After we wait (up to 30 seconds) or leave the page for a later run.
- You are responsible for how you use the results: follow the data-protection and anti-spam laws that apply to you and to the people you contact (for example GDPR in the EU/UK, CAN-SPAM in the US, CASL in Canada), and respect each website's terms.
- Business contact details can still be personal data (e.g. a named employee's email). Keep only what you need and honour opt-out requests.
- Want company-level data only? Turn on **Role mailboxes only (no personal emails)** (`roleEmailsOnly`): only shared inboxes such as info@, sales@ or support@ are returned; addresses that look like a named person are dropped before output.

### FAQ

**Does the Actor collect usage data?** The Actor sends one anonymous usage counter per run (input type, item count range, outcome) to improve the tool; no URLs, content or account data.

**Can I enrich Google Maps results and get the business emails?** Yes. Put the Google Maps run in **Dataset ID (optional)** and its website field (e.g. `website`) in **Dataset field**; each company's own website is read for emails, phone numbers and social profiles. Add **Dataset fields to keep** (e.g. `title`, `placeId`) so every row maps back to its place; places without a website or with only an Instagram, Facebook or business directory page (411.ca, Yellow Pages) get a row and are not charged.

**What does domain enrichment return for each company?** Paste bare domains into **Company domains**, one per line. Each company gets a name, a one-line AI description, an industry and business model, emails, phone numbers, social profiles, postal address, logo, language, a country guess and the tech stack, all read from its own website.

**Do I also need a contact extractor and a tech stack detector for lead enrichment?** No. This Actor already includes contact details and the tech stack, so for a Google Maps list you do not need to run Website Email Extractor for Google Maps Leads & Domain Lists and Website Tech Stack Detector as well. The tech stack is included in the price and can be turned off with **Include the tech stack**.

**Does company enrichment include LinkedIn data or personal emails?** No. Everything comes from the company's public website; there is no LinkedIn, registry or employee data. It does not guess, generate or verify personal email addresses. Social profiles are the company's own pages linked from its website; a person's LinkedIn profile (`linkedin.com/in/...`) is not returned.

**What happens with a directory page, a parked domain, a sign-in page or a site that blocks bots?** The company still gets a row, and it is not charged. A page on a social network or business directory (Facebook, Instagram, 411.ca, Yellow Pages, Yelp) is `errorType: "not_a_website"`; a parked, for-sale or "coming soon" domain has `siteStatus` `parked` or `placeholder`; a site that redirects to a sign-in page (or is one, or answers HTTP 401) is `login_required`; a site that serves a bot challenge or refuses every route is `blocked`; a domain that does not exist is `network`; with `respectRobotsTxt` on, a home page that robots.txt disallows is `robots_disallowed` (by default robots.txt does not stop a site you list; this Actor never logs in or solves a CAPTCHA). If the website is JavaScript-only, it is read once more in a browser before it is judged.

### Support

Open an issue in the **Issues** tab with the domain. Issues are checked regularly.

Found this useful? A short review on the Store helps other users find it.

# Actor input Schema

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

Main input: JSON array of bare domains, website URLs or company names, e.g. {"domains":\["stripe.com","www.bakery.co.uk"]}; paste one per line in the form. https:// is added to bare domains; www and non-www forms are one company. Names are accepted here or in companyNames and used only when a matching website is verified. Invalid lines and unverified names get free rows. Up to 50,000 per run. Alternatives: startUrls or datasetId.

## `companyNames` (type: `array`):

Company names without a website, one per line, e.g. Basecamp or Hofbräuhaus München. For each name the likely domains (basecamp.com, ...) are checked, and one is used only when its own site name (schema.org, og:site_name or page title) is that company and it is not a parked page; the row says how it was found (websiteFromName). Names without a verified website get a free row (company_not_found). A company is charged only when it is enriched, like any other.

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

Company websites as a URL list; you can also upload or link a text/CSV file.

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

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

On (default): company names (in "Company names" or in the domain list) are looked up to their website as described there. Off: they get a free invalid row.

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

Dataset ID from another Actor run, e.g. {"datasetId":"abc123"}, or select a dataset in the form. Google Maps, directory, contact-extractor and email-validator datasets are supported; each row needs a business website or company domain (not a Google Maps link or personal email domain). The website field is detected automatically or set with datasetField. Lead columns are retained; duplicate companies are enriched once.

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

Field with the website or domain, e.g. "website" (dot paths such as "company.url" work). Empty = the first URL field found (url, website, link, domain...), or the website column for Google Maps, Email Extractor and Email Validator datasets.

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

Fields copied from each dataset item into the output's "source" object, e.g. title, placeId, address, so every row maps back to the record it came from (for example a Google Maps place).

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

Off (default): every business email published on the website is returned, including addresses that look like a named person (anna.meier@...). On: only shared role mailboxes (info@, contact@, sales@, support@, office@...) are returned; personal-looking addresses are dropped before output.

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

The home page plus likely contact and about pages. The price per company is the same however many pages are read.

## `includeTechStack` (type: `boolean`):

Detect the website's technologies (CMS, e-commerce, analytics, CDN...) from the home page. Included in the price.

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

How many companies are processed at the same time.

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

After this time no further pages of a website are started and the pages read so far are used. If even the home page is not read by then, the company 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.

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

Off (default): the websites you list are read even where their robots.txt disallows automated crawlers, as when you open them yourself. Every request still identifies itself as TidyTools; we never log in (sign-in pages are reported as login_required, free), never solve CAPTCHAs and respect Retry-After. On: pages robots.txt disallows for our crawler (TidyToolsContacts) are skipped, and a website whose home page is disallowed is a free robots_disallowed row.

## Actor input object example

```json
{
  "domains": [
    "basecamp.com",
    "hofbraeuhaus.de"
  ],
  "findWebsitesForNames": true,
  "roleEmailsOnly": false,
  "maxPagesPerSite": 3,
  "includeTechStack": true,
  "maxConcurrency": 5,
  "siteTimeoutSecs": 120,
  "httpVia": "auto",
  "respectRobotsTxt": false
}
```

# Actor output Schema

## `companies` (type: `string`):

No description

## `salesLeads` (type: `string`):

No description

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

No description

## `allColumns` (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": [
        "basecamp.com",
        "hofbraeuhaus.de"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("tidytools/company-website-enrichment").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": [
        "basecamp.com",
        "hofbraeuhaus.de",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("tidytools/company-website-enrichment").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": [
    "basecamp.com",
    "hofbraeuhaus.de"
  ]
}' |
apify call tidytools/company-website-enrichment --silent --output-dataset

```

## MCP server setup

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

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/RcPuCbK3BPevD8Uau/builds/cIhdFtJi0ARLOGagi/openapi.json
