Website Contact Extractor — Emails, Phones & Socials avatar

Website Contact Extractor — Emails, Phones & Socials

Pricing

from $0.80 / 1,000 domain with contacts founds

Go to Apify Store
Website Contact Extractor — Emails, Phones & Socials

Website Contact Extractor — Emails, Phones & Socials

Give it a list of website domains and get back business emails, phone numbers, LinkedIn and social profiles, the contact page URL and postal address, one row per domain. HTTP only, no browser, no login. You only pay for domains where something is found.

Pricing

from $0.80 / 1,000 domain with contacts founds

Rating

0.0

(0)

Developer

Insight Solutions

Insight Solutions

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

a day ago

Last modified

Share

Extract emails from a list of websites. Give this Actor a list of domains and get back the business email addresses, phone numbers, LinkedIn and social profiles, contact-page URL and postal address for each one — one row per domain, as JSON, CSV or Excel.

It reads the sites' own public pages over plain HTTP: no browser, no proxy, no login, and no email verification pings of any kind. A typical domain costs one to four page fetches and finishes in a couple of seconds; 1,000 domains typically finish in a few minutes.

You are only charged for domains where we actually find something, plus a flat $0.0005 per run. Blocked sites, dead domains and sites with no published contact details cost you nothing.

Try it in 30 seconds

{ "domains": ["apify.com", "mozilla.org"] }

Bare domains, www. hosts and full URLs all work. A bare domain is resolved automatically across four permutations — https://, https://www., http://, http://www. — so a site that only answers on one of them is still found.

Digging harder on sites that bury their details:

{
"domains": ["example-manufacturer.de", "example-agency.co.uk"],
"maxPagesPerDomain": 8,
"contactPathHints": ["/contact", "/kontakt", "/impressum", "/about", "/legal"]
}

What we extract from each website (and what we never touch)

This is the important section, so it is near the top.

We extract

  • Role-based business addressesinfo@, contact@, sales@, support@, hello@, careers@, press@, office@, admin@, and their equivalents in German, French, Spanish, Italian, Portuguese and Polish (kontakt@, vertrieb@, ventas@, …).
  • Officer and owner mailboxesowner@, founder@, ceo@, cto@, director@, manager@, geschaeftsfuehrer@. These name a post, not a person: for an SMB list, owner@plumbingco.example is usually the address you wanted.
  • Non-personal generic addressesteam@, bookings@, orders@, enquiries@. Business functions, not people.
  • Published business phone numbers, normalised to E.164.
  • Company social profiles — LinkedIn, X/Twitter, Facebook, Instagram, YouTube, GitHub.
  • The contact page URL, the postal address, and whether the site has a contact form.

We do not extract, by default

  • Anything that is not a recognised business function. The filter is an allowlist, not a name detector: an address survives only if its local part matches a known role or generic business word (trailing digits ignored, so sales2@ and shop2024@ still match). Everything else — jane.doe@, anna.k@, sarah_jones88@, wolfgang@ — is classified personal and dropped before the row is written, and a given name anywhere in the local part beats a role word, so anna.sales@ is personal too. Opt in with includePersonalEmails. The cost is deliberate: an unusual but genuine business mailbox (kundenbetreuung@) can be dropped too. Losing a business address costs you recall; emitting a natural person's address costs you a GDPR problem.
  • Contact details attached to a person in structured data. If a page's JSON-LD marks an email as belonging to a Person — a blog author, a quoted employee — we never read it, even when includePersonalEmails is on. There is no switch for that one.
  • Names, in any field, ever. And noreply@, donotreply@, bounce@ and other addresses nobody can be reached at.

We never log in to anything, never ping SMTP or call any email-verification API, never ignore robots.txt (a site that disallows us gets found: false, error: "robots_disallowed" and is not charged), and never resell or retain your results.

Use cases

  • Enrich a B2B lead list with contact details — feed the domains from your CRM and get emails, phones and LinkedIn back, one row per company.
  • Get emails from a list of websites you already have — a scraped directory, a conference exhibitor list, a Google Maps export.
  • Build an outbound list for an SMB verticalowner@, info@ and a phone number is usually all a local-business campaign needs.
  • Find the LinkedIn and social profiles for a set of companies — attributed to the company, not scraped from a share button.
  • Fill in missing phone numbers and postal addresses for records you already hold, normalised to E.164.
  • Verify a supplier or partner list is still live — dead, parked and blocked domains come back labelled, free of charge.

How it compares

  • You pay per domain where contacts were found, not per input row and not per page crawled. On a 1,000-domain list with an 85% hit rate that is 850 billable rows — the other 150 are in your dataset with a reason, at $0.00.
  • ≈$1.50 per 1,000 domains with contacts, where listings in this category run to $25 per 1,000 results.
  • GDPR posture by default, not as an afterthought. Role-based allowlist, no names in any field, Person-marked JSON-LD never read, no SMTP probing, robots.txt honoured and reported.
  • One row per input domain, always — including failures, with a machine-readable error. You can tell "we could not reach it" apart from "there was nothing there".
  • No headless browser. That is a deliberate trade: sites that assemble an address in JavaScript are a miss, and in exchange the Actor costs a fraction of a browser-based crawler and runs in seconds.

Input reference

FieldTypeDefaultWhat it does
domainsarray of strings(required)Domains or URLs, one per line. Email addresses and bare IPs are rejected with a diagnostic row and never charged
maxPagesPerDomaininteger 1–104Total pages fetched per domain, including the home page. The rest of the budget goes to the best-scoring contact/about/imprint links. Costs nothing extra — pricing is per domain, not per page
contactPathHintsarray of strings/contact, /contact-us, /about, /imprint, /impressum, /legalPaths we prefer to follow. Prefix matching, case-insensitive. Anchor text mentioning contact/about/imprint/kontakt is followed regardless
includePersonalEmailsbooleanfalseAlso return person-shaped addresses. Read the GDPR note below before enabling
extractPhonesbooleantrueParse tel: links, JSON-LD telephone and visible text; normalise to E.164
extractSocialsbooleantrueDetect the six social platforms, normalised to the profile root
requestTimeoutSecsinteger 5–12020Per-request timeout. Lower it to move faster through lists with many dead domains
maxRunSecsinteger 30–3600240Wall-clock budget for the run. When reached, no further domains are started; the status message says how many were skipped, and skipped domains are never charged

Output reference

One row per input domain — always, including failures. The dataset is never empty.

{
"domain": "alpha-robotics.example",
"inputUrl": "alpha-robotics.example",
"resolvedUrl": "https://www.alpha-robotics.example/",
"redirectChain": ["https://www.alpha-robotics.example/"],
"httpStatus": 200,
"pageTitle": "Alpha Robotics — Industrial automation",
"emails": [
{ "address": "info@alpha-robotics.example", "type": "role", "foundOn": "https://www.alpha-robotics.example/" },
{ "address": "bookings@alpha-robotics.example", "type": "generic", "foundOn": "https://www.alpha-robotics.example/contact" }
],
"phones": [{ "raw": "+44 20 7946 0958", "e164": "+442079460958", "foundOn": "https://www.alpha-robotics.example/" }],
"socials": {
"linkedin": "https://linkedin.com/company/alpha-robotics",
"twitter": "https://twitter.com/alpharobotics",
"facebook": null, "instagram": null, "youtube": null,
"github": "https://github.com/alpha-robotics"
},
"contactPageUrl": "https://www.alpha-robotics.example/contact",
"contactPageConfirmed": true,
"addressText": "12 Kingsway, London, WC2B 6UN, GB",
"hasContactForm": true,
"pagesFetched": ["https://www.alpha-robotics.example/", "https://www.alpha-robotics.example/contact"],
"found": true, "error": null,
"source": "www.alpha-robotics.example",
"sourceUrl": "https://www.alpha-robotics.example/",
"scrapedAt": "2026-09-08T09:14:02.117Z"
}

Field notes

  • emails[].typerole (a named business function), generic (business-flavoured but off the canonical role list), or personal (only when you opted in). Sorted role first. Page text is read the way a browser lays it out, so an address at the end of a link never fuses with the next label.
  • phones[].e164null when the number could not be validated. Numbers from a tel: link are kept even when they do not validate, because the site declared them; numbers from body text are kept only when they validate, so prices, years and postcodes do not end up in your CRM.
  • socials — always all six keys, null where nothing was found. Share buttons, tweet-intent links and embedded videos are ignored. A profile is reported only with evidence the company owns it: JSON-LD sameAs, a handle matching the site's domain, or the same profile root on two pages. For LinkedIn, X/Twitter, YouTube and GitHub a link in the site's own header, footer or nav counts too; Facebook and Instagram keep the stricter rule.
  • contactPageUrl / contactPageConfirmed — the best contact/imprint page identified, and whether we fetched it. Only a confirmed page counts as a hit, so a "Contact us" anchor that 404s never makes a domain billable.
  • foundtrue when we found at least one email, phone, attributable social profile or a contact page we fetched. This is the flag that decides whether you are charged.
  • errornull on success, otherwise no_contacts, blocked, robots_disallowed, dns, timeout, tls, connection, not_html, too_large, invalid_input, or http_<status>.

Accuracy and coverage

Roughly 85% of ordinary business websites yield at least one contact. That is an estimate derived from the failure modes below, not a published benchmark — your hit rate depends on the list you feed in. SMB service businesses do better than enterprise SaaS companies that route everything through a form.

What stops usRoughlyWhat you see
Site publishes no address at all — contact form only5–10%hasContactForm: true, contactPageUrl set; billable only if that page was fetched
Cloudflare / bot wall / WAF blocking datacenter IPs10–15%error: "blocked"
Email assembled by JavaScript at runtimea few %address simply absent
Dead, parked or expired domainvaries by listerror: "dns"
robots.txt disallows usrareerror: "robots_disallowed"

What we handle that simpler extractors miss: Cloudflare data-cfemail encoding is decoded (arithmetic on served markup — we execute no JavaScript); name [at] example [dot] com and its (at) / {at} variants are de-obfuscated; JSON-LD Organization and ContactPoint blocks are parsed, including @graph and nested publisher; mailto: links with ?subject=, display names and multiple recipients parse correctly; contact pages are followed by path and anchor text in six languages; non-Latin imprint pages work, with no ASCII assumptions.

When a run reports FAILED. If at least one domain yielded contacts, or every domain answered and simply published nothing, the run SUCCEEDS — "no contacts published" is a correct answer, with a free row for it. Only when every domain failed on DNS, a timeout, TLS, a refused connection or a bot wall, and nothing was extracted, does the run finish FAILED — reporting that as a success would hide a broken Actor, or a blocked egress IP, behind a dataset full of diagnostics. Every diagnostic row is still written, and no domain-with-contacts event is charged on it.

Pricing

EventFreeBronzeSilverGold
actor-start — once per run, after input validation$0.0005$0.0005$0.0005$0.0005
domain-with-contacts — per domain, only when contacts are found$0.0015$0.0015$0.0011$0.0008

≈ $1.50 per 1,000 domains with contacts. Nothing for the misses.

Worked example. One run over 1,000 domains on the Free or Bronze tier, of which 850 yield at least one contact:

actor-start 1 × $0.0005 = $0.0005
domain-with-contacts 850 × $0.0015 = $1.2750
───────
$1.2755

The 150 domains that were blocked, dead or empty are in your dataset, and cost $0.00.

You are not charged when the site is behind a bot wall (blocked), the domain does not resolve (dns), robots.txt tells us not to, the request times out or TLS fails, the input was an email address or an IP (invalid_input), the site loaded fine and simply publishes no contact details (no_contacts), we saw a "Contact us" link but the page budget ran out before fetching it, or the run hit maxRunSecs or your run-cost limit before reaching that domain. The one flat charge is actor-start, billed once per run and only after your input has been validated — a run with no usable domain in it exits before charging anything at all.

Integrations and use from code

One JSON object in, one flat array out. The Integrations tab pushes results straight to Slack, a webhook, Zapier, Make, Google Sheets, Snowflake or BigQuery, and Apify Schedules run it unattended — contact details change slowly, so monthly is usually plenty.

curl -X POST "https://api.apify.com/v2/acts/insight.solutions~website-contact-extractor/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"domains":["apify.com","mozilla.org"],"maxPagesPerDomain":4}'
# pip install apify-client
from apify_client import ApifyClient
client = ApifyClient("<APIFY_TOKEN>")
run = client.actor("insight.solutions/website-contact-extractor").call(run_input={
"domains": ["apify.com", "mozilla.org", "example-agency.co.uk"],
"maxPagesPerDomain": 6,
})
for row in client.dataset(run["defaultDatasetId"]).iterate_items():
if row["found"]:
emails = ", ".join(e["address"] for e in row["emails"])
print(row["domain"], emails, row.get("socials", {}).get("linkedin"), sep=" | ")

GDPR / CCPA notes for operators

Read this before you switch includePersonalEmails on.

  • The default output is business contact data. Role-based addresses like info@ and sales@ identify a function, not a person — the least contentious category of contact data there is, and most EU regulators treat them as such.
  • includePersonalEmails changes that. jane.doe@company.com identifies a natural person and is personal data under the GDPR and the CCPA/CPRA, regardless of having been published on a public web page.
  • If you enable it, you are the controller — not Apify, and not this Actor's developer. You need a lawful basis (usually legitimate interest, with a balancing test you can produce on request), you owe the Article 13/14 notice at first contact, and you must honour access, rectification, erasure and objection requests.
  • Marketing to these addresses is a separate question. The GDPR governs whether you may hold the data; ePrivacy and national law govern whether you may email it, and several member states require prior consent for B2B email marketing regardless of your GDPR basis. CAN-SPAM is more permissive but still requires accurate headers and a working opt-out.
  • We do not verify deliverability, so treat every address as unconfirmed.

Nothing here is legal advice. If you are running outbound at scale into the EU, get some.

FAQ

Do you check whether the email address actually works? No, and we will not add it. Verification means connecting to the recipient's mail server and probing for a mailbox — a different legal and technical posture, and what gets IP ranges blacklisted. We report exactly what the website publishes.

Why did a site come back blocked? Its WAF refused a datacenter IP, or served a bot-check interstitial instead of the page. We detect both — including the awkward case where the interstitial is served with HTTP 200 — and report blocked rather than a row full of Cloudflare boilerplate. You are not charged.

Why is found true when emails is empty? We found a phone number, a social profile attributable to the company, or a contact page we actually fetched. Any one is a usable route to the company. Filter on emails.length > 0 if you only want rows with an email.

My run says FAILED but there are rows in the dataset. Every domain failed to load — DNS, timeout, TLS, refused connection or a bot wall — and nothing was extracted. The rows are the diagnostics, with the reason in error. No domain was charged. If a big, varied list fails this way, something upstream is wrong.

Can I feed it URLs from a sitemap or a crawl? Yes. Full URLs are used as given, fetched directly rather than through the permutation ladder. Duplicates are not merged, so de-duplicate your list first if you do not want to pay twice for the same company.

Does it follow links beyond the pages I allowed? No. maxPagesPerDomain is a hard cap on total fetches per domain, home page included, and we follow only same-registrable-domain links that score as contact-ish. There is no recursive crawl.

  • Public pages only. Everything this Actor reads is served to any anonymous visitor. We never authenticate, never supply cookies or session tokens, and never accept credentials as input.
  • We honour robots.txt on every host we touch, and report a refusal as a result rather than working around it. No SMTP probing, no verification APIs.
  • We are not a data broker. This Actor holds no database and sells no list. It reads pages on demand and writes the result to your dataset on your account. We retain nothing.
  • Not affiliated with, endorsed by or sponsored by any website you point it at, nor by Cloudflare, LinkedIn, X/Twitter, Meta, Google, YouTube or GitHub. Platform names are used descriptively, to say which link we detected.
  • You are responsible for how you use the output. Compliance with the GDPR, the CCPA, ePrivacy, CAN-SPAM and applicable marketing law sits with you as the operator.

Our other Actors

HTTP-only data Actors — no browsers, no proxies, charge-on-success.

ActorWhat it does
Career Site Jobs APIEvery open role from Greenhouse, Lever, Ashby, Workable and 10+ other ATS platforms, normalized
New Job Postings MonitorOnly the roles that opened, closed or changed since your last run — hiring signals for sales and recruiting
SEC EDGAR APISEC filings, normalized XBRL financials and full-text search, by ticker or CIK
Wayback Machine ToolkitArchived URLs, archived page text, and a diff of one page between two dates