# Website Contact Scraper: Email, Phone, Social (`snow_leo_data/website-contact-email-phone-social-scraper-lead-extractor`) Actor

One row per website, not per page: emails, phone numbers in E.164, 31 social platforms, postal address, opening hours and VAT id. The mail domain is MX-checked, Cloudflare-obfuscated addresses are decoded, and no browser or proxy is involved.

- **URL**: https://apify.com/snow\_leo\_data/website-contact-email-phone-social-scraper-lead-extractor.md
- **Developed by:** [Snow Leo Data](https://apify.com/snow_leo_data) (community)
- **Categories:** Lead generation, SEO tools, Marketing
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.84 / 1,000 website rows

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?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Website Contact Scraper: Email, Phone and Social Profile Extractor

Give this Actor a list of websites and get back **one clean row per website**:
every email address on the site, every phone number in international `+` form,
social profiles across 31 platforms, the postal address, the opening hours and
the company identifiers - and, for each address, whether its domain can receive
mail at all.

It reads the sites with plain HTTP requests. **No browser, no proxy, no API
key.** That is not a limitation dressed up as a feature: it is the reason one
website costs one row instead of twenty billed page loads.

#### The number that matters most: half the contacts are not on the home page

Measured on **135 live business websites** - shops, offices, workshops and
restaurants in Berlin, Bristol and Austin, addresses taken from OpenStreetMap,
list stored in `tools/sites_sample.json` so the measurement can be repeated
command-for-command with `python3 tools/measure.py`:

| | Home page only | Home page + contact pages |
|---|---|---|
| Sites where an email was found | **31** | **57** |
| Email addresses found | **36** | **154** |
| Unique addresses | 35 | 134 |
| Sites with a phone number | 34 | 41 |
| Phone numbers found | 67 | 225 |

**Reading one page finds an email on 31 of 135 sites. Reading the contact and
imprint pages as well finds one on 57** - and returns **4.28 times** as many
addresses. A scraper that stops at the home page loses that silently: the run
is green, the row is just empty.

The opposite extreme - crawling the whole site - is what you pay for elsewhere.
This Actor ranks the links instead: `impressum` and `imprint` weigh most,
`contact` and `kontakt` next, then `about`, `team`, `support`, `locations`,
`privacy`. The 135-site run fetched **530 pages for 99 reachable sites**, and
every one of them is inside the single row you are billed for.

#### Addresses a plain text scrape cannot see

Four separate channels feed the `emails` column, and the row records which one
found each address in `emails_detail[].method`:

- **`mailto`** - the most reliable, and present on far fewer sites than people expect;
- **`text`** and **`markup`** - the address written in the page, including inside attributes and inline data;
- **`jsonld`** - `schema.org` markup, where a company often publishes an address that appears nowhere in the visible text;
- **`cloudflare`** - Cloudflare's email protection, which **replaces the address in the HTML with a hex string** and rebuilds it with JavaScript in the browser. A scraper without a browser sees nothing there. This one decodes it: **25 addresses hidden this way on 3 of the 135 sites**;
- **`obfuscated`** - `info (at) example (dot) com` and its variants, matched on word boundaries so that "Attractive package at good rates" is never mistaken for an address.

Junk that looks like an address is removed before you are charged for it:
image names such as `logo@2x.png`, tracking keys on `sentry.io`, theme
placeholders such as `info@example.com` and 23 other placeholder domains,
hex blobs and UUIDs.

#### One row per website, not one per page

The largest Actor in this niche bills **per scraped page** and prefills 20
pages per website. Its own README gives the rate: "$5 of prepaid credits ...
around 2,500 pages", which is **$2.00 per 1,000 pages**, so its own default
setting works out at **$40.00 per 1,000 websites**.

Here a website is one row. Whatever it took to read it - the home page, the
contact page, the imprint, the sitemap lookup - is inside that row. The median
price in this niche, measured across **199 priced Actors** carrying "contact",
"email" or "phone" in slug or title, is **$4.00 per 1,000 results**.

#### Does that address even work?

Every email domain is checked against DNS over HTTPS, once per run per domain,
and the result is attached to each address:

- `mx_found` - the domain has an MX record, or an A record it can fall back to under RFC 5321;
- `mx_hosts` - the mail servers themselves;
- `mail_provider` - Google Workspace, Microsoft 365, IONOS, Zoho, Proton, Namecheap and 15 more, read from those hosts;
- `mx_note` - `nxdomain`, `no-mx-no-a`, `a-record-fallback` or `resolver-unreachable`.

**What this proves and what it does not.** A missing MX *and* a missing A
record proves the domain cannot accept mail, so that address is dead. A present
MX does **not** prove the mailbox exists. This Actor never says it does, and
there is no field here claiming a verified mailbox.

#### Phone numbers you can actually dial

Numbers arrive from `tel:` links, from `schema.org`, from WhatsApp links and
from the page text, and each one is normalised to **E.164** - `+442079460958`,
the one shape a CRM or an auto-dialer will accept.

The country is derived, never guessed: from the `+` already in the number,
then from `addressCountry` in the site's structured data, then from a national
domain (`.de`, `.co.uk`; `.io` and `.co` are deliberately not treated as
countries). With no country available, the number stays in `phones_raw` and
`e164` is `null` - a wrong number costs more than an empty field. Set
`defaultCountry` to fill those in when you know the list is from one country.

Two repairs come from live pages: a postcode glued to a number
(`78702 512.912.1612`) and a house number trailing one (`(512) 215 9778 1807`)
are trimmed group by group until a valid number appears, and never by adding or
reordering digits. And the same number written three ways on three pages
becomes **one** contact, deduplicated on the E.164 form - so you are not billed
three times for it.

#### What else comes back in the row

The dataset schema declares **87 columns**. Besides contacts:

- **Company from `schema.org`**: `company_name`, `legal_name`, `schema_type`, `company_description`, `logo_url`, `slogan`, `vat_id`, `tax_id`, `lei_code`, `duns`, `price_range`, `founding_date`. Structured company data was present on **37 of the 135 sites**.
- **Postal address split into fields**: `street`, `city`, `region`, `postal_code`, `country`, `country_code`, plus `latitude` and `longitude` - on **22 sites** in the sample.
- **`opening_hours`**, one line per day (`Mo 09:00-17:30`) - on **12 sites**.
- **Site facts useful for qualifying a lead**: `platform` (**15 different site builders and shop systems appeared in the sample**, from WordPress and Shopify to Wix, Webflow, Duda and GoDaddy), `generator`, `trackers`, `page_title`, `meta_description`, `language`.
- **`has_contact_form` and `contact_form_url`** - **23 sites** carry a contact form, which is where you write when there is no address at all.
- **`contact_page_url`, `imprint_page_url`, `pages`** - so you can see exactly where each fact came from.
- **The outcome of the request itself**: `reachable`, `http_status`, `blocked`, `error`, `redirected_off_domain`, `insecure_tls`, and `scraped_at` so a row read months later still says when it was true.

Social profiles land both as plain columns (`linkedin`, `twitter`, `facebook`,
`instagram`, `youtube`, `tiktok`, `pinterest`, `telegram`, `whatsapp`,
`discord`, `github`, `threads`, `snapchat`, `reddit`, `vk`, `xing`, `medium`,
`vimeo`, `yelp`, `trustpilot`) and in `social_profiles` covering **31
platforms**. Share buttons, `/intent/` links and platform home pages are not
profiles and are dropped: **361 profiles across 82 sites** in the sample are
real ones.

#### A site that refuses still gets a row

By default, a site that answers 403 or no longer resolves comes back as a row
saying so - `reachable: false`, `blocked: true`, `http_status`, `error`. Your
list keeps its length and every address is accounted for. Switch on
`skipUnreachableSites` to drop those rows instead and not pay for them.

Measured by running the 135-site sample through this very Actor, run
`UeFdbC59fuHm7AqgQ` on Apify servers, with no proxy and no browser: **12 of 135
were refused and 100 opened**, and 23 no longer resolve at all - stale entries in
the source list rather than blocks. That run fetched 539 pages, found 261 email
addresses, 508 phone numbers and 396 social profiles, checked 86 mail domains,
and returned 121 rows after 14 duplicate domains were merged into the ones you
already pay for.

#### Monitor mode: pay only for what changed

Set `onlyChanged` and the Actor remembers what each site returned - in a
**named** key-value store, so the memory survives between runs, which the
default store does not. On the next run only sites whose contacts changed are
written, tagged `NEW` or `UPDATED`. Nothing changed means **an empty dataset
and no charge**, and the log says so in plain words rather than leaving you
guessing.

The fingerprint covers emails, phone numbers, social profiles, company name
and reachability - not the page title or the tracker list, which change on
every site deployment and would mark everything as changed. `emitUnchanged`
returns the unchanged sites too, tagged `UNCHANGED`; `monitorStoreName` keeps
several lists apart.

This is the shape of demand in this niche: the benchmark Actor has **218.7 runs
per user**, and the whole niche - **562 Actors, 257,019 users, 21,188,146
runs** - averages **82.4 runs per user**. People put lists on a schedule.

#### When the dataset is empty, the Actor says why

An empty dataset with a green run is the worst thing a paid Actor can do to
you. Every empty result here carries a named reason in the `REPORT` record and
a warning in the log. Ten of them:

- `no-start-urls` - nothing was given to read;
- `all-urls-invalid` - nothing in the list is a web address;
- `deliverable-without-mx-check` - `onlyDeliverableEmail` is on while `checkMx` is off, so every row would be dropped whatever the sites contain;
- `email-kinds-exclude-everything` - the kinds kept and the free-mail switch exclude every possible address;
- `charge-limit-reached` - the run's charge limit is already used up;
- `all-sites-refused` - every site refused or could not be reached;
- `no-contacts-found` - the sites opened and carried no contact at all;
- `filtered-out` - contacts were found and your filters removed every row;
- `nothing-changed` - monitor mode, and nothing moved since last time;
- `nothing-collected` - nothing at all, and no filter is to blame.

The first five are decided **from the input alone, before a single byte is
downloaded**. A self-contradicting input is not something you should pay
crawling time for: the run stops, names the reason and writes the report.

#### Filters, and the trap they usually hide

Twelve filters: `onlyWithEmail`, `onlyWithPhone`, `onlyWithContacts`,
`onlyWithSocialProfile`, `skipUnreachableSites`, `onlyDeliverableEmail`,
`onlySameDomainEmail`, `skipFreeMailboxes`, `emailKinds`, `domainContains`,
`textContains`, `excludeContains`.

Two rules they all follow. **An empty field is missing information, not a
mismatch**: a word filter never drops a site simply because that site published
no structured data. And **a filter never depends on a field this run did not
collect** - the one case where it could, `onlyDeliverableEmail` without
`checkMx`, is caught before any request instead of quietly returning zero rows.

`maxItems` limits what is **written and billed**, and sites beyond the limit are
never opened. With filters on, more sites are visited than the limit, because
some rows will be dropped - but the delivered count still stops at your number.

#### Input at a glance

`startUrls` takes bare domains, full links, or link-in-bio pages.
`maxPagesPerSite` (default 6, ceiling 40) and `crawlStrategy`
(`contact-pages`, `all-links`, `homepage-only`) set how deep to go;
`useSitemap` and `guessContactPaths` find contact pages that the home page
never links to; `sameDomain` keeps the crawl on the site; `scrapeIframes` opens
embedded frames; `followLinkInBioPages` reads the sites behind a bio page;
`checkMx` and `defaultCountry` control data quality; `concurrency` and
`requestTimeoutSecs` control speed; `compactOutput` drops empty fields and the
detail arrays for AI agents and spreadsheets; `onlyChanged`, `emitUnchanged`
and `monitorStoreName` run the monitor. **29 input fields in total.**

#### Honest limitations

Written out rather than left for you to discover:

1. **No browser.** Pages painted entirely by JavaScript give up less here than they would to a browser-based scraper. The trade is deliberate: it is what makes a whole website cost one row.
2. **No proxy.** A site that blocks datacentre addresses stays blocked. Measured from Apify servers on the 135-site sample: 12 answered 401 or 403 and stay out of reach.
3. **No employee records.** This Actor reads what the website publishes. It does not buy names, job titles or personal work emails from a B2B database, and there is no add-on here that does.
4. **No social profile enrichment.** Profile links are returned; follower counts and profile details from Instagram, TikTok or YouTube are not.
5. **Mail check stops at the domain.** MX or A record present or absent. No mailbox-level verification, and no claim of one.
6. **Crawl ceiling of 40 pages per site.** Deep, unstructured sites that hide contacts below that are out of reach.
7. **Phone country is derived, not guessed.** With no `+`, no structured address and no national domain, `e164` stays `null` and the raw string is still returned.
8. **`schema.org` data depends on the site.** 37 of 135 sites published it; the rest return the contact fields without the company block.

A line-by-line comparison with the strongest Actor in the niche - every input,
every output field, every mode - is in `COMPARISON.md`.

#### FAQ

**Can I paste a plain domain instead of a full URL?**
Yes. `example.com`, `www.example.com` and `https://example.com/page` all work,
and the Actor tries `www` and non-`www`, plus plain HTTP, before calling a site
unreachable.

**What happens to a `linktr.ee` link?**
A link-in-bio page has no contacts of its own, so the Actor reads the sites it
links to instead. Thirteen such hosts are recognised, including `bio.link`,
`beacons.ai`, `taplink.cc` and `carrd.co`. Turn it off with
`followLinkInBioPages`.

**Why did a site come back with no email at all?**
Usually because the address is only in a contact form, or in an image, or the
page is drawn by JavaScript. Check `has_contact_form` - if it is `true`, the
company took messages through a form rather than publishing a mailbox. Raising
`maxPagesPerSite` or switching `crawlStrategy` to `all-links` also helps on
large sites.

**Is a row with `mx_found: true` a verified email?**
No. It means the domain can receive mail. Mailbox-level verification is not
part of this Actor, and no field here claims it.

**How many pages will one site cost me?**
None, directly - you are billed per row, and a row is a website. For reference,
the 135-site measurement fetched 530 pages in total.

**Can I run this on a schedule without paying for the same data twice?**
That is exactly what `onlyChanged` is for. Unchanged sites are not written and
not billed, and the run reports `nothing-changed` rather than looking broken.

**Does it respect the sites it reads?**
It reads only public pages, at most 40 per site, with a per-host slow-down that
doubles after any `429` and is shared by all threads. No logins, no forms
submitted, no personal data bought from anywhere.

**What is in the REPORT record?**
Sites requested, opened, blocked and unreachable; pages fetched; emails, phone
numbers and social profiles found; duplicates dropped before billing; how many
rows each filter removed; how many mail domains were checked; which hosts
failed and why; and the named empty reason when the dataset is empty.

**Which fields should I look at first for cold outreach?**
`primary_email` (a role mailbox on the site's own domain is preferred over a
personal one on free mail), `deliverable_emails`, `primary_phone`,
`company_name`, `city` and `platform`.

**Is the data GDPR-safe?**
Everything returned is published by the site itself on a public page. Personal
addresses are labelled `personal` in `emails_detail[].kind` so you can drop
them with `emailKinds` if your legal basis only covers role mailboxes. What you
do with the data is your responsibility.

# Actor input Schema

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

Websites to read. A bare domain (<code>example.com</code>), a full link, or a link-in-bio page such as <code>linktr.ee/name</code> all work. One row comes back per address, even when the site refuses the request - the row then says why.

## `maxItems` (type: `integer`):

Hard stop on how many rows are written and charged. <code>0</code> means no limit. Sites beyond the limit are never opened, so the limit saves time as well as money.

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

How many pages of one site may be opened. Measured on 135 live business sites: reading only the home page finds an email on far fewer sites than reading the contact and imprint pages as well. Six is a good balance; raise it for large sites.

## `crawlStrategy` (type: `string`):

<b>contact-pages</b> ranks links by how likely they lead to contacts (contact, imprint, about, team, locations) and opens the best ones. <b>all-links</b> opens same-site links in page order. <b>homepage-only</b> reads one page.

## `useSitemap` (type: `boolean`):

When the home page has no visible link to a contact page - common on sites whose menu is drawn by a script - look for one in <code>robots.txt</code> and <code>sitemap.xml</code>.

## `guessContactPaths` (type: `boolean`):

If nothing contact-like was found in the links or the sitemap, try well-known paths such as <code>/contact</code>, <code>/kontakt</code> and <code>/impressum</code>.

## `sameDomain` (type: `boolean`):

Follow only links that stay on the same domain. Link-in-bio pages are the deliberate exception: their whole point is to lead elsewhere.

## `followLinkInBioPages` (type: `boolean`):

For <code>linktr.ee</code>, <code>bio.link</code>, <code>beacons.ai</code> and ten similar hosts, read the linked sites instead of the bio page itself, which has no contacts of its own.

## `scrapeIframes` (type: `boolean`):

Also open frames embedded in the page. Some site builders put the footer with the address into a frame. Off by default because frames often hold adverts.

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

Ask DNS whether each email domain can receive mail at all, and name the mail provider. A missing MX and A record proves the address is undeliverable; a present MX does not prove the mailbox exists, and this Actor never claims it does.

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

Two-letter country code used to build the international <code>+</code> form when the site itself gives no clue - no country domain and no address in its structured data. Leave empty to keep such numbers raw rather than guess.

## `onlyWithEmail` (type: `boolean`):

Drop rows where no email was found.

## `onlyWithPhone` (type: `boolean`):

Drop rows where no phone number was found, in any form.

## `onlyWithContacts` (type: `boolean`):

Keep a row if it has an email, a phone number or a social profile.

## `onlyWithSocialProfile` (type: `boolean`):

Drop rows where no social media profile was found on the site.

## `skipUnreachableSites` (type: `boolean`):

By default a site that refuses the request or no longer exists still gets a row, so your list stays the same length and says what happened to each address. Switch this on to drop those rows instead - and not pay for them.

## `onlyDeliverableEmail` (type: `boolean`):

Keep a row only if at least one email sits on a domain that has an MX (or A) record. Needs <b>Check the mail domain</b> switched on - with it off the run stops before opening anything and says so, instead of quietly returning nothing.

## `onlySameDomainEmail` (type: `boolean`):

Drop addresses hosted elsewhere - an agency mailbox, a marketplace, a web designer's own address in the footer.

## `skipFreeMailboxes` (type: `boolean`):

Drop gmail.com, gmx.de, outlook.com and 50 other consumer mail hosts.

## `emailKinds` (type: `array`):

<b>role</b> - info@, sales@, support@ and 70 more. <b>personal</b> - looks like a person's name. <b>generic</b> - anything else. <b>noreply</b> - automated senders. Empty means keep all kinds.

## `domainContains` (type: `array`):

Keep only sites whose domain contains one of these words.

## `textContains` (type: `array`):

Keep only sites whose title, description or company name contains one of these words.

## `excludeContains` (type: `array`):

Drop sites whose address, title, description or company name contains one of these words.

## `onlyChanged` (type: `boolean`):

Remember what each site returned and, on the next run, write only the sites whose contacts changed. Nothing changed means an empty dataset and no charge - the log says so in plain words. Memory lives in a named key-value store, so it survives between runs.

## `emitUnchanged` (type: `boolean`):

In monitor mode, write unchanged sites as well, tagged <code>UNCHANGED</code>.

## `monitorStoreName` (type: `string`):

Name of the key-value store holding the monitor memory. Use different names to keep several lists apart.

## `compactOutput` (type: `boolean`):

Drop empty fields and the per-address detail arrays. Smaller rows for AI agents and spreadsheets.

## `concurrency` (type: `integer`):

How many sites are read at the same time.

## `requestTimeoutSecs` (type: `integer`):

How long to wait for one page before giving up on it. Small business sites on cheap hosting are slow; twenty seconds covered every site that answered at all in the 135-site measurement.

## Actor input object example

```json
{
  "startUrls": [
    {
      "url": "https://www.austinfilm.org"
    },
    {
      "url": "https://jeffreysofaustin.com"
    },
    {
      "url": "https://www.yarddog.com"
    }
  ],
  "maxItems": 100,
  "maxPagesPerSite": 6,
  "crawlStrategy": "contact-pages",
  "useSitemap": true,
  "guessContactPaths": true,
  "sameDomain": true,
  "followLinkInBioPages": true,
  "scrapeIframes": false,
  "checkMx": true,
  "defaultCountry": "US",
  "onlyWithEmail": false,
  "onlyWithPhone": false,
  "onlyWithContacts": false,
  "onlyWithSocialProfile": false,
  "skipUnreachableSites": false,
  "onlyDeliverableEmail": false,
  "onlySameDomainEmail": false,
  "skipFreeMailboxes": false,
  "emailKinds": [],
  "domainContains": [],
  "textContains": [],
  "excludeContains": [],
  "onlyChanged": false,
  "emitUnchanged": false,
  "monitorStoreName": "website-contacts-state",
  "compactOutput": false,
  "concurrency": 6,
  "requestTimeoutSecs": 20
}
```

# Actor output Schema

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

Every row delivered by this run, one per website.

## `report` (type: `string`):

Sites opened, blocked and unreachable, pages fetched, contacts found, duplicates dropped before billing, what each filter removed and - when the dataset is empty - the named reason why.

# 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 = {
    "startUrls": [
        {
            "url": "https://www.austinfilm.org"
        },
        {
            "url": "https://jeffreysofaustin.com"
        },
        {
            "url": "https://www.yarddog.com"
        }
    ],
    "maxItems": 100,
    "maxPagesPerSite": 6,
    "crawlStrategy": "contact-pages"
};

// Run the Actor and wait for it to finish
const run = await client.actor("snow_leo_data/website-contact-email-phone-social-scraper-lead-extractor").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "startUrls": [
        { "url": "https://www.austinfilm.org" },
        { "url": "https://jeffreysofaustin.com" },
        { "url": "https://www.yarddog.com" },
    ],
    "maxItems": 100,
    "maxPagesPerSite": 6,
    "crawlStrategy": "contact-pages",
}

# Run the Actor and wait for it to finish
run = client.actor("snow_leo_data/website-contact-email-phone-social-scraper-lead-extractor").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "startUrls": [
    {
      "url": "https://www.austinfilm.org"
    },
    {
      "url": "https://jeffreysofaustin.com"
    },
    {
      "url": "https://www.yarddog.com"
    }
  ],
  "maxItems": 100,
  "maxPagesPerSite": 6,
  "crawlStrategy": "contact-pages"
}' |
apify call snow_leo_data/website-contact-email-phone-social-scraper-lead-extractor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,snow_leo_data/website-contact-email-phone-social-scraper-lead-extractor"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/yVW2xE2XC1jMYf8Lf/builds/fkvvcJNdgqMiLpD7I/openapi.json
