Website Contact Email Finder avatar

Website Contact Email Finder

Pricing

$1.49 / 1,000 website with a new emails

Go to Apify Store
Website Contact Email Finder

Website Contact Email Finder

Give it website URLs; get the contact emails on each site's home and contact pages, with the page each came from. USD 0.00149 per website with a new email, nothing otherwise. New = not yet delivered in this run, or in earlier runs using the same dedupeStoreName. No deliverability check.

Pricing

$1.49 / 1,000 website with a new emails

Rating

0.0

(0)

Developer

Datadelight

Datadelight

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

3 days ago

Last modified

Categories

Security

Limited permissions

Share

Give it a list of website addresses. For each website it reads the home page and a few contact-style pages on that same site and returns the email addresses written there, with the page each one came from so you can open it and check. You are charged only for a website where a new contact email for that business was found. A run that finds nothing costs nothing to run.

It works on websites you already have. It does not search Google Maps, it does not read social networks, and it does not check whether an address accepts mail.

What you get

One row per website with a new email:

{
"website": "https://acme-plumbing.example",
"domain": "acme-plumbing.example",
"finalUrl": "https://acme-plumbing.example/",
"status": "email_found",
"reason": null,
"primaryEmail": "info@acme-plumbing.example",
"emails": [
{ "email": "info@acme-plumbing.example", "foundOn": "https://acme-plumbing.example/", "source": "mailto", "onSiteDomain": true, "relation": "own_domain" },
{ "email": "sales@acme-plumbing.example", "foundOn": "https://acme-plumbing.example/contact", "source": "text", "onSiteDomain": true, "relation": "own_domain" },
{ "email": "accounts@acme-plumbing.example", "foundOn": "https://acme-plumbing.example/contact", "source": "obfuscated", "onSiteDomain": true, "relation": "own_domain" }
],
"otherEmails": [],
"pagesFetched": 3,
"crawledAt": "2026-11-01T12:00:00Z"
}

emails holds the addresses you pay for. Each says why it counts in relation:

  • own_domain: on the website's own domain (or a parent or subdomain of it).
  • structured_contact: an email or ContactPoint in the site's own JSON-LD, even on another domain (a diocese's admissions office).
  • labelled_contact: written on the page next to words that label it as a way to reach the business ("email Joe at ...", "Reception: ...", "Bookings: ..."), even on another domain (a business Gmail, a parent company).
  • mailto_webmail: a mail link to a consumer mailbox (Gmail, Outlook, Yahoo ...) that is not a credit line.
  • contact_page: unlabelled text in the main content (not header, footer or navigation) of one of the contact pages read.

onSiteDomain is false for the others. The rest go to otherEmails with a reason, are not charged and are not remembered: credit_line (words like "Website by", "Template by", "Powered by", "Designed by" just before the address, so a web agency, theme author or developer is never your contact) and no_contact_context (another domain, nothing says it is the business's contact: an unlabelled address in a quote or a footer). A website whose only addresses are of those kinds has status only_other_domain_emails and is saved only when includeEmptyRows is on. Addresses of website builders, hosting platforms and standards bodies (for example support@wix.com) are dropped. This is a judgement from the words around an address, not a guarantee: a contact in a footer with no label is not billed, and a label near an address that is not the business's can still bill it (an agency's address after its own "Email" line is not billed when its domain name matches the studio just credited, in the same block or in the block just after the credit, in either word order and ignoring a second-level suffix (co.uk, me.uk, ltd.uk, plc.uk, sch.uk, com.au, id.au, asn.au, geek.nz, uk.com, web.id, asso.fr; a listed set, extended by the rule that for any other domain the label before its last two is compared as well, so an unlisted one- or two-label tail such as .xyz.example is covered but a three-label suffix such as k12.ca.us is not), or the hosting service for a tenant host (lena-studio of lena-studio.github.io, lena-studio.workers.dev), but a credited name that does not match its domain, or an address further from the credit, is judged by the label). An address on a tenant host of a shared hosting service (name.herokuapp.com, name.github.io) is judged by its context like any other address: an explicit label, structured contact data or a contact page can bill it, an unlabelled mention or a credit line cannot, and a bare suffix such as info@github.io or info@me.uk has no mailboxes and never bills. Addresses at site-builder and platform domains (wix.com, wixsite.com, myshopify.com, shopify.com, weebly.com, webflow.com, wordpress.com) are dropped without a row or detail, even when the website itself is hosted there: those domains do not host mailboxes, so a shop on name.myshopify.com that lists owner@name.myshopify.com is reported as having no emails. This is a words heuristic over the page text; it does not check that a mailbox exists, receives mail or belongs to the business (no SMTP, MX or ownership verification). Check foundOn.

source says how the address was found: mailto (a mail link), text (written on the page), structured_data (JSON-LD), cloudflare_protected (Cloudflare's "[email protected]" cloak, decoded on the page), obfuscated (written as name [at] domain [dot] com).

The key-value store record OUTPUT summarises the run: websites given, invalid, duplicated, skipped as already seen, crawled, with emails, without, unreachable (by reason), pages fetched and the number of billable events the actor asked the platform to charge. That last number equals the number of saved rows.

Input

FieldDefaultMeaning
websitesrequiredWebsite URLs (or bare domains). One entry per website.
maxResults100Stop after this many websites with new emails. Also your spending cap for the run.
maxPagesPerWebsite3Home page plus up to this many minus one contact, about, imprint (impressum), team or support pages on the same site (1 to 8).
maxEmailsPerWebsite5Emails kept per website, best first (1 to 20).
dedupeStoreNameemptyA key-value store name. Websites and emails delivered in earlier runs with the same name are skipped and not charged again.
includeEmptyRowsfalseAlso save a row, never charged, for websites with no new email, with status and reason.
respectRobotsTxttrueSkip pages robots.txt disallows.
maxConcurrency, requestTimeoutSecs, siteTimeoutSecs10, 15, 60Parallel websites, per-page wait, per-website limit.

Duplicates are handled for you: the same website given twice (with or without www., http or https, a trailing slash or tracking parameters) is checked once, and an email already delivered, in this run or in an earlier run that used the same dedupeStoreName, is not delivered or charged again.

If a run is interrupted

Before each billable row is saved and charged, the actor writes a "pending" record of that website to a key-value store, and marks it done afterwards. The store is your dedupeStoreName store, or, with no name, the run's own default store (which survives a platform restart of the same run, but not a new run). If the run dies in between (killed, out of memory, a charge response that never arrived), the next start treats that website as possibly delivered: it is not fetched, delivered or charged again, and OUTPUT lists it under possiblyDelivered. The cost of that safety is that such a website may be missing from your dataset (the run died before saving it) or saved but not charged. Check your dataset for it; to retry it deliberately, run it with a different dedupeStoreName. The Apify SDK gives no idempotency key for saving and charging a row together, so this protection comes from the actor's own records, not the platform; whether the platform ever charged a row whose response was lost is not something the actor can read. Do not run two runs on one dedupeStoreName at the same time. Without a dedupeStoreName, separate runs do not remember each other.

What is charged

One event, website-with-email, per row saved with at least one contact email you have not been given before (see relation above). Nothing is charged for a website with no email, a website whose only addresses are credit lines (web agency, site builder, template author, website credit) or unlabelled addresses on another domain, an unreachable website, a duplicate website, a website whose emails were all delivered before, or a run that finds nothing. Your maximum total charge setting for the run is respected: the actor stops instead of exceeding it.

What it will not do

  • No Google Maps, no search. It does not find businesses. Bring the websites (from your own list or another tool).
  • No social networks and no logged-in pages. Public web pages only.
  • No JavaScript rendering. An address that only appears after a script runs is not found. Plain HTML pages, including mail links, text, JSON-LD and Cloudflare-cloaked addresses, are read.
  • No deliverability checks. An address is returned if it is written on the page and is well formed. It may bounce, belong to a person who left, or be a role address (info@).
  • Not every site has an email. Many publish a form only; those return no row. A site whose only visible address is an unlabelled one on another domain returns no billable row (it is in otherEmails when includeEmptyRows is on).
  • It only fetches http and https pages on ports 80 and 443. Addresses with a username or password, private or internal network addresses, and links to files are refused. It sends no cookies, uses no proxies and identifies itself as WebsiteContactEmailFinder. A page is read up to 1.5 MB; compressed responses (gzip, including several members, and deflate) are decoded with the same limit, and a page that cannot be decoded is reported as unreachable and not charged. Pages are read in one pass without building a document tree and scanned in time proportional to their size, so a huge or hostile page costs the same as any other of that size. If a site's robots.txt cannot be read (server error or connection failure) the site is skipped as robots_unavailable, as RFC 9309 requires, and its row says why in reasonDetail (for example connection_error: ConnectError (refused or unreachable), timeout: ReadTimeout or timeout: ConnectTimeout, tls_error: SSLCertVerificationError (untrusted certificate), http_error: 503; never a URL, address or credential); robots_disallowed means its rules forbid it. A home-page link is followed as a contact page by its address or by its visible text ("Get in touch", "Reach us", "Contáctanos", ...), within the page budget, and only on the same site.

Using it responsibly

Emails on a public page are still personal data when they belong to a person. You are responsible for lawful use, including your obligations when you contact people (for example PECR and GDPR in the UK and EU, CAN-SPAM in the US). The actor does not store the emails it finds anywhere except in your run's dataset; the optional remember store holds only hashes of delivered emails and the domains of delivered websites.

Run it locally

uv sync
uv run pytest # offline, fixtures/ and a local SDK simulation only; takes about a minute
apify run # needs the Apify CLI; put the input in storage/key_value_stores/default/INPUT.json