# Website Contacts & Decision Makers Scraper (`truswen/website-contacts-decision-makers`) Actor

Turn a list of company websites into contact rows: emails, phone numbers (validated, typed), social profiles, address, VAT ID, plus the owners and managers named on the site. Works on any country's websites, pairs with Google Maps Scraper output.

- **URL**: https://apify.com/truswen/website-contacts-decision-makers.md
- **Developed by:** [Benjamin Zsigri](https://apify.com/truswen) (community)
- **Categories:** Lead generation, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $6.00 / 1,000 website scanneds

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

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

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

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

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

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

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

# README

## Website Contacts & Decision Makers Scraper

Turn a list of company websites into a clean contact sheet. For every website you get **one row** with:

- **Emails**, labelled as role mailbox (`info@`, `sales@`), personal (`jane.smith@`), generic or freemail, with the page each one was found on
- **Phone numbers**, validated and formatted in international E.164 (`+441614960123`), typed as landline, mobile or service number, with fax numbers kept apart
- **Decision makers**: owners, founders, managing directors, executives, board members and department heads named on the website, with their **role**, a **role category** and, when the site publishes it, their **email**
- **Social profiles**: LinkedIn, Facebook, Instagram, X/Twitter, YouTube, TikTok, Pinterest, XING, GitHub
- **Company name, postal address, VAT ID**, website language and description

It works on websites from **any country** (English, German, French, Spanish, Italian, Dutch, Portuguese, Polish, Nordic, Hungarian and Czech pages are understood) and it is built to take the output of **Google Maps Scraper** or any other dataset with a website column.

### Who it is for

- **Sales and lead generation**: you have a list of businesses (from Google Maps, a directory, a trade fair, your CRM) and need a phone number, an email and the name of the person in charge.
- **Agencies** building prospect lists for clients.
- **CRM enrichment**: fill in missing emails, phones, social profiles and VAT IDs for accounts you already have.
- **Market research**: who owns and runs the companies in a niche.

### How it works

1. Each website is visited starting from its homepage.
2. The crawler picks the pages most likely to hold contact details first: **contact**, **imprint / legal notice** (Impressum, mentions légales, aviso legal), **team / leadership**, **about**. It never opens login, basket, blog, shop or privacy pages.
3. Every page is read for emails, phone numbers, social links, structured data (schema.org) and people with a role.
4. Everything found is merged into one row per website, ranked (the company's own domain first, role mailboxes before personal ones, the main phone number first, the most senior person first).

A typical small or mid-size business website needs 4 to 8 pages. You set the maximum with **Max pages per website**.

#### What the Actor does NOT do

- It does not log in anywhere and it does not solve or bypass CAPTCHAs.
- It respects **robots.txt**. A website that disallows crawling is reported as `robots_blocked` and is not charged.
- It does not decode **email protection** scripts (such as Cloudflare's): the row is flagged with `email_hidden_by_protection: true` instead. Emails written as `name [at] company [dot] com` are plain text shown to every visitor and are read.
- It does not guess emails (no `firstname.lastname@` invention) and it does not use LinkedIn. Only what the company publishes on its own website is returned.
- Customer testimonials and reviews are kept apart from the company's people: names right under a quote or in a "what our customers say" section are skipped.

### Input

The simplest input is a list of websites:

```json
{
  "websites": ["https://www.example-plumbing.co.uk", "example-dental.com"],
  "findDecisionMakers": true,
  "maxDecisionMakersPerWebsite": 3
}
```

Or point the Actor at a dataset, for example the output of a **Google Maps Scraper** run:

```json
{
  "datasetId": "YOUR_DATASET_ID",
  "maxPagesPerWebsite": 8
}
```

The website field is detected automatically (`website`, `url`, `domain`, `companyWebsite`, …) or you can name it in **Website field in the dataset**. A Google Maps listing link is never taken for the website; items without a website are skipped. Fields such as `title`, `placeId`, `address`, `city`, `countryCode` and `categoryName` are copied into `source_item`, so you can join the results back to your list.

| Field | What it does |
|---|---|
| `websites` | Company websites, one per line. Duplicates are scanned once. |
| `datasetId` | Import websites from a dataset (e.g. Google Maps Scraper output). |
| `findDecisionMakers` | Recognise owners, executives and managers named on the site. |
| `maxDecisionMakersPerWebsite` | Most senior first. Caps the cost per website. |
| `includeOtherPeople` | Other named people with a role (staff, contact persons). Free. |
| `defaultCountry` | Country for local phone numbers written without a prefix. Without it, the dataset's `countryCode` or the website's country domain is used. |
| `maxPagesPerWebsite` | Requests per website, homepage included (default 8). |
| `maxConcurrency` | Websites scanned in parallel (default 20). |
| `proxyConfiguration` | Optional. Most company websites answer direct requests. |

### Output

One row per website. Overview columns (`primary_email`, `primary_phone`, `decision_maker_name`, `decision_maker_role`, `decision_maker_email`, `linkedin`, …) make the CSV and Excel export usable without unpacking arrays; the full lists are in the JSON.

```json
{
  "input_url": "example-plumbing.co.uk",
  "domain": "example-plumbing.co.uk",
  "website": "https://www.example-plumbing.co.uk/",
  "status": "ok",
  "company_name": "Example Plumbing Ltd",
  "language": "en",
  "emails": [
    { "email": "hello@example-plumbing.co.uk", "type": "role", "same_domain": true, "source_url": "https://www.example-plumbing.co.uk/contact/" },
    { "email": "jane.smith@example-plumbing.co.uk", "type": "named", "same_domain": true, "source_url": "https://www.example-plumbing.co.uk/meet-the-team/" }
  ],
  "phones": [
    { "number": "+441614960123", "type": "landline", "country": "GB", "source_url": "https://www.example-plumbing.co.uk/" },
    { "number": "+447911123456", "type": "mobile", "country": "GB", "source_url": "https://www.example-plumbing.co.uk/contact/" }
  ],
  "fax_numbers": [],
  "socials": { "facebook": "https://facebook.com/exampleplumbing", "linkedin": "https://linkedin.com/company/example-plumbing" },
  "address": { "street": "12 Mill Lane", "postal_code": "M1 2AB", "city": "Manchester", "region": null, "country": "GB" },
  "vat_ids": [{ "id": "GB123456789", "country": "GB" }],
  "decision_makers": [
    {
      "name": "Jane Smith",
      "first_name": "Jane",
      "last_name": "Smith",
      "salutation": null,
      "role": "Founder & Managing Director",
      "role_category": "owner",
      "is_decision_maker": true,
      "email": "jane.smith@example-plumbing.co.uk",
      "source_url": "https://www.example-plumbing.co.uk/meet-the-team/",
      "extraction_method": "name_above_role"
    }
  ],
  "other_people": [{ "name": "Tom Baker", "role": "Gas Safe Engineer", "role_category": "staff", "is_decision_maker": false }],
  "email_hidden_by_protection": false,
  "primary_email": "hello@example-plumbing.co.uk",
  "primary_phone": "+441614960123",
  "decision_maker_name": "Jane Smith",
  "decision_maker_role": "Founder & Managing Director",
  "decision_maker_email": "jane.smith@example-plumbing.co.uk",
  "pages_crawled": 4,
  "scraped_at": "2026-10-01T09:00:00.000Z"
}
```

#### Role categories

| `role_category` | Examples | Decision maker |
|---|---|---|
| `owner` | Owner, Founder, Co-founder, Managing Partner, Inhaber, Gründer, Fondateur | yes |
| `executive` | CEO, Managing Director, General Manager, Geschäftsführer, Directeur général | yes |
| `board` | Chairman, Board member, Non-Executive Director, Verwaltungsrat | yes |
| `authorized_representative` | Legal representative named in the legal notice | yes |
| `department_head` | Head of Sales, Sales Director, Director of Marketing, Office Manager, Vertriebsleiter | yes |
| `content_responsible`, `contact` | Responsible for content, contact person | no |
| `staff` | Engineer, Consultant, Plumber, Account Manager | no |

#### Status values

| `status` | Meaning | Charged |
|---|---|---|
| `ok` | The website answered and was read | yes |
| `robots_blocked` | robots.txt disallows crawling | no |
| `blocked` | The site refuses automated requests (HTTP 401/403/429) or redirects elsewhere | no |
| `not_a_company_website` | A social profile, directory or marketplace page, not the company's own site | no |
| `unreachable` / `http_error` / `not_html` / `invalid_url` / `error` | The website could not be read | no |

### Pricing

This Actor uses **pay per event** pricing: you pay for results, not for compute time.

- **Website scanned**: charged once per website that answered (`status: ok`), whatever was found on it.
- **Decision maker found**: charged per decision maker returned. Set **Max decision makers per website** to cap it, or switch decision makers off.

Websites that could not be read are listed in the results with the reason, **free of charge**. Other named people (`other_people`) are free. The current prices are on the **Pricing** tab.

The Actor respects your **maximum cost per run**: when the limit is reached it stops before starting another website, and it never returns a decision maker it could not charge for.

### Use it with Google Maps Scraper

1. Run **Google Maps Scraper** for your niche and area (e.g. "plumbers in Manchester").
2. Copy the run's dataset ID.
3. Start this Actor with `datasetId` set to it.

Each row keeps the place's `title`, `placeId`, `address` and `countryCode` in `source_item`, so you get the place, its website contacts and its owner in one table. You can chain both Actors automatically with an Apify **integration** (run this Actor when the Maps run succeeds).

### Where your list can come from

- **Google Maps Scraper**: set `datasetId` to its run's dataset. This Actor never searches Google Maps itself; it works on the dataset you bring.
- **A CSV file or spreadsheet**: paste the website column into `websites`, one per line.
- **Your CRM**: export the accounts and paste the website column, or push the rows into an Apify dataset with the API and set `datasetId` to it. `datasetUrlField` names the column when it is not one of the usual names.
- **Make, n8n, Zapier or Clay**: start the Actor through the Apify API (Apify also offers ready-made modules for some of these tools), pass `websites` in the input and read the run's dataset as the output.

### Tips

- **Speed and cost**: 6 to 8 pages per website find contact details on the vast majority of small business websites. Raise it for large corporate sites with deep team sections.
- **Local phone numbers** written without a country prefix are only returned when the country is known (from `defaultCountry`, the dataset's `countryCode` or the country domain such as `.de` or `.co.uk`). A number is never guessed into the wrong country.
- **Blocked websites**: if many of your websites end as `blocked`, try a proxy in the input.
- **Resuming**: if a run is migrated by the platform, it continues where it stopped without scanning or charging the finished websites again.

### Is it legal to scrape contact details?

This Actor reads only pages that the company publishes openly on its own website, respects robots.txt and does not bypass any login, CAPTCHA or email protection. Business contact details of companies are generally public information. However, names and personal work emails of people are **personal data** under the GDPR (EU/UK) and similar laws. If you store or use them, you need a lawful basis (for B2B outreach usually legitimate interest), you must tell people where you got their data, and you must respect opt-outs and local direct-marketing rules (e.g. PECR in the UK). If you are unsure, ask a lawyer. You are responsible for how you use the data.

### Limitations

- Websites that render all content with JavaScript only (no server-side HTML) may return few results.
- People are recognised from their name and role as written on the page. A person introduced only in a paragraph of prose, or only in an image, is not found.
- The postal address comes from schema.org structured data when the site provides it.

### Feedback

Found a website where the result is wrong or incomplete? Open an issue on the **Issues** tab with the URL. Real examples are the fastest way to improve the extraction.

### Need it done for you?

Want this connected to your CRM or workflow, or adapted to your market? Tribloc, the team behind this Actor, builds these setups. Get in touch at [tribloc.co.uk](https://tribloc.co.uk/).

# Actor input Schema

## `websites` (type: `array`):

Company websites, one per line. A bare domain (acme.com) works too. Each website is scanned once, even if it appears several times.

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

ID of a dataset whose items contain a website field, for example the default dataset of a Google Maps Scraper or Google Search run. Fields like title, placeId, address and countryCode are copied into `source_item`, so you can join the rows back. Pick the dataset (or paste its ID); the Actor is given read access to that one dataset only.

## `datasetUrlField` (type: `string`):

Name of the field that holds the website URL. Leave empty to try the usual names: website, url, websiteUrl, domain, companyWebsite, homepage.

## `findDecisionMakers` (type: `boolean`):

Recognise owners, founders, managing directors, executives, board members and department heads named on the website (team, about, imprint and contact pages), with their role and, when published, their email.

## `maxDecisionMakersPerWebsite` (type: `integer`):

The most senior people first (owner, then executive, board, managing director, department head). Each returned decision maker is a charged event, so this caps the cost per website.

## `includeOtherPeople` (type: `boolean`):

Also list other people named on the website with a role (staff, contact persons). Free of charge.

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

Two-letter country code (US, GB, DE, ...) used to read local phone numbers written without a country prefix. Leave empty to use the dataset item's countryCode or the website's country domain (.de, .co.uk, ...); without either, only numbers written with an international prefix are returned.

## `maxPagesPerWebsite` (type: `integer`):

Requests per website, homepage included. The crawler picks the pages most likely to hold contact details first (contact, imprint/legal notice, team, about). 6 to 10 covers almost every small and mid-size business website.

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

How many websites are scanned at the same time. Each single website is always requested politely, one page at a time.

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

Optional. Most company websites answer direct requests; use a proxy only if many of your websites return blocked or HTTP errors.

## Actor input object example

```json
{
  "websites": [
    "https://www.sennheiser.com",
    "https://www.stihl.de",
    "https://www.mailerlite.com"
  ],
  "findDecisionMakers": true,
  "maxDecisionMakersPerWebsite": 3,
  "includeOtherPeople": true,
  "maxPagesPerWebsite": 8,
  "maxConcurrency": 20,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

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

One row per website: company, best email and phone, top decision maker, social profiles.

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

Complete rows: all emails, phones, social profiles, address, VAT IDs, decision makers and other people, with the page each fact came from.

## `summary` (type: `string`):

Websites processed by status, decision makers found, charged events, requests.

# 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 = {
    "websites": [
        "https://www.sennheiser.com",
        "https://www.stihl.de",
        "https://www.mailerlite.com"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("truswen/website-contacts-decision-makers").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 = { "websites": [
        "https://www.sennheiser.com",
        "https://www.stihl.de",
        "https://www.mailerlite.com",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("truswen/website-contacts-decision-makers").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 '{
  "websites": [
    "https://www.sennheiser.com",
    "https://www.stihl.de",
    "https://www.mailerlite.com"
  ]
}' |
apify call truswen/website-contacts-decision-makers --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,truswen/website-contacts-decision-makers"
        }
    }
}
```

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/5ZQNaJPngIED2fwwp/builds/Ml0Xtc0we1ggvOSTd/openapi.json
