Website Contact Scraper - Email & Phone Finder avatar

Website Contact Scraper - Email & Phone Finder

Pricing

from $2.50 / 1,000 website with contacts founds

Go to Apify Store
Website Contact Scraper - Email & Phone Finder

Website Contact Scraper - Email & Phone Finder

Find emails, phones and social profiles on any list of websites. $5 per 1,000 websites with a contact: blocked, dead or empty sites are free. One clean row per domain, with the source page for every contact.

Pricing

from $2.50 / 1,000 website with contacts founds

Rating

0.0

(0)

Developer

Sumitr Mardy

Sumitr Mardy

Maintained by Community

Actor stats

1

Bookmarked

2

Total users

1

Monthly active users

9 days ago

Last modified

Categories

Features

Limited permissions

Share

Website Contact Finder — pay only for sites with contacts

For local-business prospecting, first collect websites with Google Maps Scraper, pass its website values as domains here, then send collected emails to Email Verifier.

Give it a list of domains. Get back one row per domain with the emails, phone numbers and social profiles published on that website, plus the business's own structured data when it has any.

You are charged only for domains where a contact was actually found. A domain that is blocked, dead, or simply publishes no contact details costs you nothing.


Why that one sentence matters

Every contact scraper hits the same ceiling: roughly a third of small-business websites publish an email address, and a third of them sit behind a bot wall. That is not a quality difference between tools — it is the web.

The difference is what you pay for the misses. The usual model bills per page opened, so a domain that turns out to be empty still costs you three or four pages. This actor bills per domain that yields a contact.

Measured on the same 100 random SME domains:

pages billeddomains with a contactyou pay for
per-page scraper33674336 pages
this actor—7777 contacts

Both find a contact on about three domains in four — the yield is level. What differs is that one bills 336 pages for it and the other bills 77 results.

Cost per contact found, at an equal page budget:

FreeGold
per-page scraper ($2.00 → $1.05 / 1k pages)$0.0091$0.0048
this actor$0.0060$0.0030

About 1.5× cheaper per contact, and the gap is wider on lists with a lot of dead or protected domains, because those are free here.

What you get per domain

{
"domain": "mud-dock.co.uk",
"url": "https://www.mud-dock.co.uk/",
"status": "contacts_found",
"sub_status": "email_and_phone",
"emails": [
{
"email": "cafe@mud-dock.co.uk",
"type": "generic", // generic | personal | unknown
"is_free_provider": false, // gmail/yahoo/...
"on_site_domain": true, // on the domain we crawled
"source_url": "https://www.mud-dock.co.uk/contact-us/"
}
],
"phones": [
{
"e164": "+441179292151", // normalised, dialable
"raw": "0117 929 2151",
"country": "GB",
"confidence": "high", // high = tel: link or structured data
"source_url": "https://www.mud-dock.co.uk/"
}
],
"socials": [{ "network": "facebook", "url": "https://www.facebook.com/muddock" }],
"organization": { // from schema.org data on the page
"name": "Mud Dock Cycleworks", "legal_name": "Mud Dock Ltd",
"street": "40 The Grove", "city": "Bristol", "region": "",
"postal_code": "BS1 4RB", "country": "GB", "type": "LocalBusiness"
},
"checks": { "dns_resolved": true, "homepage_ok": true, "bot_challenge": false,
"json_ld_found": true, "mailto_found": true,
"obfuscated_email_decoded": false, "tel_link_found": true,
"contact_page_reached": true, "email_on_site_domain": true,
"has_contact_form": true },
"pages": ["https://www.mud-dock.co.uk/", "https://www.mud-dock.co.uk/contact-us/"],
"pages_crawled": 2,
"found_at": "2026-10-19T08:00:00.000Z",
"duration_ms": 1840
}

The schema is a contract

  • Exactly one row per input domain, including domains that returned nothing. Join on your input list without wondering which rows went missing.
  • status and sub_status are never null and never empty.
  • emails, phones, socials and pages are always arrays.
  • organization always has all 8 string keys; unknown fields are "".
  • checks always has all 10 boolean keys; a check that was never reached is false, never null. The reason lives in sub_status.

Status values

statussub_statusmeaning
contacts_foundemail_and_phone / email_only / phone_onlycharged
no_contactsno_contact_publishedpages read fine, the site publishes no address or number
blockedbot_challenge / http_errora bot wall or a 4xx answered us. Worth retrying later
unreachabledns_error / tls_error / timeoutdomain is dead, mis-configured, or too slow
unreachableinternal_errorour fault; error says what happened

Only contacts_found is ever charged, plus Apify's standard start event ($0.00005 per GB of memory, $0.0001 per run at the default 2 GB), in return for which Apify covers the first 5 seconds of compute.

How it finds things

  1. Homepage first. On the measured sample it carried most of the finds on its own, so most domains cost one page.
  2. Then the site's own contact links, then /contact, /contact-us, /about, /about-us, /team, /impressum.
  3. Crawling stops as soon as both an email and a phone number are found.
  4. schema.org/JSON-LD is read first where present — an address the owner put there for machines is better than one scraped out of a footer.
  5. Obfuscated addresses are decoded: Cloudflare's data-cfemail, HTML entities, and name [at] example [dot] com.
  6. Phone numbers are normalised to E.164. The dialling plan comes from structured data, then the ccTLD. On a .com or .org the page decides: its language region and any UK or Canadian postcode nominate a country, and the numbers on the page vote between that and your defaultCountry — so a Bristol business on .com is read as UK, while a stray UK postcode on a US page loses to the US numbers around it. Company and VAT numbers are rejected by validation rather than by guesswork, and two numbers on two lines are never read as one.
  7. Apify Proxy (datacenter) is on by default. Six domains in the 100-domain sample answered a direct connection with 403 or nothing and were fine through rotating IPs. A blocked page is retried once on a fresh IP.

What it does not do

  • No headless browser. Measured: exactly one domain in 100 was a JavaScript shell with contact details only in the rendered DOM. A browser would multiply the cost to fix one row in a hundred.
  • No guessing. If a site publishes no address, you get no_contacts, not info@ invented from the domain.
  • No person-level enrichment. This reads what the website publishes.

Expected yield, stated honestly

On 100 random SME domains (shops, trades and offices in the US and UK, sampled from OpenStreetMap), measured 2026-10-22 on Apify with the datacenter proxy on:

a contact (email or phone) : 77%
an email address : 51%
a phone number : 73%
a social profile : 73%
publishes no contact at all: 13%
blocked by a bot wall : 7%
dead or unreachable : 3%
pages read : 3.33 per domain

Run side by side with the leading per-page scraper over the same 100 domains:

emailphonecontact (email or phone)
per-page scraper387374
this actor517377

Compared domain by domain rather than by totals:

  • Email: this actor is ahead. 18 domains where only it found an address, 5 the other way.
  • Phone: level. 4 against 4. Until build 0.1.10 the other scraper led here by 15 against 5: this actor read a UK business on a .com domain with US dialling rules and dropped every number, and read two numbers on two lines as one long invalid one. Both are fixed.
  • Any contact: level. 5 against 2 — not enough to call a difference.

The other scraper also returns "uncertain" numbers that include year ranges, decimals and Unix timestamps; counting those it reaches 77 phones and 77 contacts. The figures above leave out anything that could not be dialled. Every phone this actor returns is validated and normalised to E.164.

The other scraper's run is from 2026-10-19 and this one from 2026-10-22. Sites change between days, so treat differences of a few domains as noise.

Roughly a third of the misses on both sides are sites that publish no address anywhere, which no scraper can fix.

Input

fielddefaultnotes
domains—domains or URLs; duplicates on the same host collapse to one row and one charge
startUrls—the same thing in Apify's URL-list format, for chaining
concurrency15cost follows total runtime, so raising this does not raise the bill
maxPagesPerDomain5a budget, not a target; crawling stops early on success
timeoutMs8000a slow host is reported, not waited on
useProxytrueturn it off and expect more blocked rows
allowInsecureTlstruesmall business sites let certificates expire; only public pages are read and nothing is ever submitted
defaultCountryUSthe fallback dialling plan for local-format numbers, used only when neither the domain, the structured data nor the page itself points to a country

Verifying the emails

Addresses found on a website are not proof that the mailbox exists. To filter bounces, pass the output through Email Verification API — SMTP, MX and honest catch-all detection, with the same no-null output contract.

Use it from AI agents (MCP)

This Actor is a tool on the Apify MCP server, so Claude, Cursor, VS Code or any MCP client can call it. Load only the tools you need: a short tool list keeps the agent from picking the wrong one.

Recommended: hosted server, OAuth sign-in (nothing secret on disk). This config loads the contact finder together with the email verifier, so an agent can find contacts and then check them in one conversation:

{
"mcpServers": {
"contact-finder": {
"url": "https://mcp.apify.com?tools=sumitr_mardy/website-contact-finder,sumitr_mardy/email-verifier"
}
}
}

With an API token instead (clients without OAuth, or unattended setups), add "headers": { "Authorization": "Bearer YOUR_APIFY_TOKEN" } next to url.

Local server (stdio):

{
"mcpServers": {
"contact-finder": {
"command": "npx",
"args": ["-y", "@apify/actors-mcp-server@latest", "--tools", "sumitr_mardy/website-contact-finder"],
"env": { "APIFY_TOKEN": "YOUR_APIFY_TOKEN" }
}
}
}

Get a token at Apify Console → API & Integrations, and never commit it. Runs are billed to the Apify account the agent signs in with, and only domains where a contact is found are charged.

Tips for agents:

  • Pass all domains in one call. Every input domain comes back as exactly one row, so results join back to the input list without guessing.
  • Read status first: contacts_found is the only charged status; anything else says why nothing came back.
  • emails[].type separates personal from generic inboxes, and every contact carries the source_url it was found on, so an agent can cite it.
  • The MCP call-actor tool waits up to 45 seconds. A long list keeps crawling after that: fetch the results with get-dataset-items and the run's datasetId.

Email addresses that identify a person are personal data under the GDPR and similar laws. This actor reads only what a website publishes to the public, but what you do with it is your responsibility: use it for business-to-business contact, honour opt-outs, and have a lawful basis before you send anything.