Website Contact Scraper avatar

Website Contact Scraper

Pricing

from $4.00 / 1,000 contact details founds

Go to Apify Store
Website Contact Scraper

Website Contact Scraper

Extract e-mail addresses, phone numbers and social profiles from any list of websites. Every address is MX-verified at no extra charge, and every value reports where on the site it was found.

Pricing

from $4.00 / 1,000 contact details founds

Rating

0.0

(0)

Developer

Mehmet

Mehmet

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

8 days ago

Last modified

Categories

Share

B2B lead generation from a list of websites. Give it the domains, get back one record per site with e-mail addresses, phone numbers and social profiles — and, for every address, where on the site it was found and whether the domain accepts mail.

Built for lead generation and CRM enrichment: feed it a prospect list, an export from your CRM, or the output of another scraper, and it returns outreach-ready contact data with the evidence attached.

No browser. No residential proxy. No login.


What makes this one different

1. E-mail verification is included, not an add-on. Every address found is checked against the domain's MX records, with the mail provider identified (Google Workspace, Microsoft 365, …), disposable domains flagged, and likely typos suggested. Other Actors on this shelf charge separately for verification. Here it is part of the base price.

2. Every value says where it came from. Each address carries method and sourcePath:

  • mailto — taken from a href="mailto:" link. The site declares this address.
  • jsonld — taken from structured data (schema.org).
  • cfemail — a mailto: link that Cloudflare obfuscated, decoded back.
  • text — found in the page body. Useful, but weaker evidence.
  • guessednot found anywhere on the site. See below.

In our measurement 87% of addresses and 79% of phone numbers came from structural markup, not body text. You can filter on that.

3. A 404 does not cost you a page. The crawl budget counts pages that actually loaded, not requests attempted. Trying /contact, /kontakt and /impressum on a site that has none is nearly free — an empty 404 has no body — so a long path list is no longer a penalty. In our measurement this alone recovered 5 more websites.

4. It finds the contact page instead of guessing its address. Most scrapers try a fixed list of paths (/contact, /about, …). This one reads the site's own navigation and follows the contact link it finds. In our measurement 19 of 67 sites published their address on a path no fixed list would have tried — /pages/contact, /outreach, /company/contact, /en/contacts, /information/contact-us/, and plain /contact/ with a trailing slash.

5. It does not invent precision. A phone number written in national format ((555) 123-4567) is returned exactly as written, with e164: null — because converting it without knowing the country would be a guess. International numbers get a proper E.164 value.

6. Share buttons are not social profiles. When a website links to its own social accounts, those links are collected and the noise around them is not: facebook.com/sharer, twitter.com/intent/tweet, pinterest.com/pin/…, t.me/s/… and platform navigation links are all excluded, so you get the company's actual profile URL rather than the share widget in the page footer.

To be clear about what this is: the Actor reads your list of company websites. It recognises links pointing to twelve social networks and returns those URLs — it does not log into, crawl or scrape any social platform itself.


Measured coverage — real numbers, not claims

We measured on two samples, because the answer depends heavily on what kind of list you feed it.

Sample A — real businesses (100 sites). Restaurants, law firms, dental practices, accountants, estate agents, ad agencies and architects, taken from OpenStreetMap entries that publish a website (London and Berlin). This is what a lead list actually looks like.

Sample B — a random slice of the web (200 sites). Drawn systematically from the Tranco top-1M. Deliberately hard: it contains parked domains, adult and gambling sites, CDN hosts and things that serve no HTML at all.

A: businessesB: random web
Sites that responded84%64%
Contact details found (of responding)82%64%
E-mail address found (of responding)71%53%
Phone number found (of responding)64%35%
With a pattern-guessed address included95%91%
Blocked by robots.txt05
Contact details found (of the whole sample)69%41%

Both were scraped from Apify's datacenter IP with no proxy, 29–30 August 2026, at a platform cost of about $0.0006 per website.

We ran sample B four times while developing this. Three consecutive builds scored 76, 77 and 79 sites — that spread is run-to-run noise, so treat these numbers as ±2 rather than as decimals. The table reports the last run, not the best one.

One caveat on sample A, stated plainly: it is London- and Berlin-weighted and restaurant-heavy, so it represents Western-European small business rather than the whole world.

Why two denominators, stated plainly. We checked every unreachable domain against a neutral DNS resolver:

Domain resolves, host is alive66%
Registered, but no web server at all29%
Genuinely gone5%

So the sites we miss are mostly alive but not serving us — they block datacenter traffic or refuse the request — not dead. Measured against every domain that actually resolves, coverage is 46% of the whole sample.

A list you supply is normally made of sites you already care about and can reach, so the "responded" column is the one that matches real use.

One more honest limit: about 26% of the sites we could not reach from Apify were reachable from an ordinary connection — those sites block datacenter IP ranges. We measured whether Apify's datacenter proxy helps: across the same 200 sites it recovered 2. It does not help, because that pool is datacenter IP space too. Only residential proxies would, and they cost enough to defeat the point of an HTTP-only scraper. The proxy input stays available, but it is off by default and we are not going to tell you it fixes this.


Input

{
"websites": ["example-company.com", "https://another-company.co.uk"],
"maxPagesPerSite": 6,
"verifyEmails": true,
"includePhones": true,
"includeSocials": true,
"includeGuessedEmails": true,
"onlyWithContact": false
}
FieldDefaultWhat it does
websitesURLs or bare domains. Each is reduced to its root.
maxPagesPerSite6Contact-intent pages to try: /, /contact, /contact-us, /about, /about-us, /impressum, then /kontakt, /imprint, /legal, …
verifyEmailstrueMX lookup for every address. No extra charge.
includePhonestrueFrom tel: links, JSON-LD, and international-format text.
includeSocialstrueCollect links the site publishes to its own social profiles (12 networks recognised). Share buttons excluded.
includeGuessedEmailstrueWhen a site publishes nothing, add info@<domain> if the domain has mail servers. Marked method: guessed.
onlyWithContactfalseSkip records for sites with no e-mail and no phone.
maxSites00 means no limit.
proxyConfigurationnoneOptional.

maxPagesPerSite is a budget, not a list: the homepage is fetched first, its contact links are followed next, and the fixed paths fill any remaining budget. The default of 6 is measured, not guessed.


Output

One record per website — including sites where nothing was found, so your input list and output list line up.

{
"input": "https://eff.org",
"url": "https://eff.org",
"domain": "eff.org",
"scrapedAt": "2026-08-28T18:33:23.916Z",
"emails": [
{
"email": "press@eff.org",
"method": "mailto",
"sourcePath": "/contact",
"isRole": false,
"isFreemail": false,
"verification": {
"status": "deliverable_domain",
"mxFound": true,
"provider": "Microsoft 365",
"isDisposable": false,
"didYouMean": null
}
}
],
"phones": [
{
"e164": "+15102438020",
"raw": "+1-510-243-8020",
"method": "text",
"sourcePath": "/contact"
}
],
"socials": [
{ "network": "facebook", "url": "https://www.facebook.com/eff", "sourcePath": "/" }
],
"pagesScanned": 3,
"requests": 4,
"skipped": null,
"hasContact": true,
"hasGuessedEmail": false,
"contactFormUrl": null
}

contactFormUrl — when a site publishes no address at all

Some websites deliberately publish no address and only offer a form. For those, the Actor reports the page where the form lives:

{ "emails": [], "phones": [], "contactFormUrl": "https://example.com/contact" }

This is not a contact detail and is not billed as one. It does not set hasContact. It exists because "nothing found" is an incomplete answer when there is a way to reach the company — we just cannot turn it into an address, because the site never published one.

Search boxes, newsletter sign-ups and login forms are excluded.

Guessed addresses — what they are, and what they are not

If a website publishes no address anywhere we looked, and its domain has mail servers, the Actor adds info@<domain> with method: "guessed" and sets hasGuessedEmail: true.

This address was not observed on the site. It is a naming-convention guess that the domain can at least receive mail. It never sets hasContact, it is billed under its own cheaper event, and you can turn it off completely with includeGuessedEmails: false.

In our measurement it raised coverage from 64% to 91% of responding sites — but those extra 27 points are guesses, and the output says so on every single record.

verification.status

ValueMeaning
deliverable_domainThe domain accepts mail. The mailbox itself was not verified.
no_mxThe domain has no mail server. Definitively undeliverable.
disposableTemporary / throwaway mail provider.
invalid_syntaxNot a valid address.
unknownThe MX lookup failed or was not run.

What this Actor will never claim: that a specific mailbox exists. SMTP handshakes are not possible from this infrastructure (port 25 is closed), so mailbox-level verification cannot be performed — and we will not report a number we cannot measure. Verification here stops at the domain's mail server, and the field names say so.

skipped

ValueMeaning
nullThe site was scraped normally.
robotsrobots.txt disallows crawling. Nothing was requested and nothing was charged.
budgetThe page or request budget ran out before a contact was found.
unreachableThe homepage could not be fetched.

Pricing

Pay per event, with volume tiers.

EventWhen it is charged
site-scrapedOnce per website, if at least one request was made.
contact-foundOnce per website, if at least one e-mail or phone number was found on the site.
contact-guessedOnce per website, only when nothing was found and a pattern-guessed address was added instead. Cheaper than contact-found.

A site that disallows crawling in robots.txt is never charged — no request was made, so no work was done. A site that responds but has no contact details is charged only the cheaper site-scraped event. A guess is never billed as a find.


Frequently asked questions

How many of my websites will return contact details? On real business websites we measured 82% of the sites that responded (69% of the whole list). On a random slice of the web it drops to 64%, because a random slice contains parked domains, CDN hosts and sites that publish nothing. Your own list will behave like the first number, not the second.

A site I know publishes an e-mail returned nothing. Why? Three usual causes. The page renders its contact details with JavaScript, and this Actor is HTTP-only. The site blocks datacenter traffic — about a quarter of the sites we could not reach were reachable from an ordinary connection. Or the address is inside an image, which no text scraper can read.

Do you verify that the mailbox exists? No, and no Actor running on this infrastructure can. Port 25 is closed here, so an SMTP handshake is impossible. Verification stops at the domain's mail servers, and the status value says exactly that: deliverable_domain, never deliverable.

What is a guessed address and can I switch it off? When a site publishes nothing anywhere we looked, and its domain has mail servers, the Actor adds info@<domain> marked method: "guessed". It was never observed on the site, it does not set hasContact, and it is billed under a separate cheaper event. Set includeGuessedEmails: false to turn it off entirely.

Is this Actor affiliated with the websites it scrapes? No. It reads public pages only, respects robots.txt per path, and never signs in. A site that disallows crawling is reported with skipped: robots and is not charged.

Can I use it to enrich a CRM export? Yes — that is the main use. Feed the domain column, and match the output back on input, which echoes exactly what you passed in. Sites with no contact details still return a row, so the two lists line up.

Limits, stated plainly

  • HTTP only. There is no headless browser, so a contact page that draws itself with JavaScript comes back empty. This is also exactly why the Actor is cheap and fast — it is a deliberate trade, not an oversight.
  • Datacenter IPs are blocked by some sites. About a quarter of the sites we could not reach were reachable from an ordinary connection. We measured whether Apify's datacenter proxy fixes this: across 200 sites it recovered 2. It does not, because that pool is datacenter space too.
  • The mailbox is never verified. Port 25 is closed on this infrastructure, so verification stops at the domain's mail servers. The status value says so: deliverable_domain, never deliverable.
  • An address inside an image cannot be read. No text scraper can.
  • robots.txt is respected per path, not just per site — and a site that disallows crawling is reported with skipped: robots and never charged.
  • Third-party addresses are filtered out (comment authors, review authors, affiliate links). An address is returned only when it belongs to the site, or when the site itself declares it in structural markup.

Notes

  • Both www. and the bare domain are tried, and http as a last resort. In our measurement this alone recovered 1 site in 10 that would otherwise have been reported unreachable.