Contact Page & Form Finder + Website Email Extractor
Pricing
from $1.40 / 1,000 domain with contacts founds
Contact Page & Form Finder + Website Email Extractor
Contact page finder and website email extractor: per site returns contactPageUrl, contactFormUrl, contactFormKind (hubspot, typeform, html...), emails, phones, socials. $0.002 per site with a result, first 25 per run free. Built with AI assistance (Claude).
Pricing
from $1.40 / 1,000 domain with contacts founds
Rating
0.0
(0)
Developer
Jeremy Larsen
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
2 hours ago
Last modified
Categories
Share
Contact Page & Form Finder: contact page URL, contact form URL, emails, phones and socials for each website, $0.002 per site with a result, first 25 per run free (empty, failed and robots-blocked sites cost nothing).
Contact Page & Form Finder + Website Email Extractor
Click Start: the prefilled input (3 small-business sites) returns real contact page and contact form URLs plus public emails and phones, free under the 25-per-run allowance.
Every row also has contactFormKind, which says what powers the contact form: html (native form), hubspot, typeform, google_forms, jotform, tally, wpforms, contact_form_7, or none, detected from iframe sources, script hosts and form markup. contactFormPageUrl is the page where an embedded form was found when it differs from contactFormUrl. Rows that include emails also carry nextStep, a link to our Bulk Email Validator so you can check the addresses before sending.
Real output for the 3 prefilled sites (public business contact data only; real local run of this build, emails and phones exactly as the sites list them, socials shown as network names):
| Website | contactPageUrl | contactFormUrl | emails | phones | socials |
|---|---|---|---|---|---|
| sunsethillvet.com | https://sunsethillvet.com/contact/ | https://sunsethillvet.com/contact/ | info@sunsethillvet.com | (206) 706-7800, (206) 706-7803 | facebook, twitter |
| barefootphysiotherapy.com.au | https://www.barefootphysiotherapy.com.au/contact/ | https://www.barefootphysiotherapy.com.au/contact/ | hello@barefootphysiotherapy.com.au | 1300842850 | |
| mainstreetbakery.com | https://mainstreetbakery.com/contact | none found | none listed | none listed | none found |
Billing: you are charged $0.002 only for a website where something was found (contact page, contact form, email, phone or social link). A website with no contacts found, an error, or a robots.txt block costs nothing, and maxCostUsd caps the spend.
Give it a list of websites or bare domains. For each one it reads the homepage plus up to two same-site contact/about/impressum pages and returns the contact page URL (contactPageUrl), the contact form URL (contactFormUrl, a page with a real message form) and the publicly listed business emails, phone numbers and social profile links. You pay only for domains where at least one contact page, contact form, email, phone or social link was actually found. Domains with nothing found, failed domains, unreachable domains and robots.txt-skipped domains are never charged, and an optional maxCostUsd input stops the run before it spends more than your budget.
Use from AI agents / MCP / n8n / Zapier
The Actor takes plain JSON and returns one flat row per site, so it works as a tool for AI agents (call it through the Apify MCP server at mcp.apify.com, or the Apify API), and as a step in n8n, Zapier or Make through the Apify integrations. Sample input:
{"urls": [{ "url": "sunsethillvet.com" }, { "url": "barefootphysiotherapy.com.au" }],"maxCostUsd": 0.5}
Fields an agent usually reads from each row: contactPageUrl, contactFormUrl, contactFormKind, emails, phones, status.
Cost: what it costs you vs other contact extractors
Pay per event, charged only for domains where something was found. Rival figures are the median estimated cost of the Store contact/email scrapers with at least 10 users in the last 7 days (7 actors, prices read from the public Store API on 2026-10-20; the per-result and per-page prices are converted assuming about 1 result or 2 pages per site, so treat them as estimates).
| Domains | This Actor (max, USD) | Rival median (est., USD) |
|---|---|---|
| 100 | $0.20 | $0.40 |
| 1,000 | $2.00 | $4.00 |
| 10,000 | $20.00 | $40.00 |
"Max" means every domain returned something. Domains with nothing found, errors and robots.txt skips cost $0, so your real cost is usually lower.
Real sample output
Three rows from real local runs of this build (apify.com, python.org and mozilla.org). Socials trimmed to the first two networks per row for length; the Actor returns all of them.
[{"input": "https://www.python.org","finalUrl": "https://www.python.org","httpStatus": 200,"contactPageUrl": "https://www.python.org/about/","contactFormUrl": null,"contactFormKind": "none","emails": [],"phones": [],"socials": {"linkedin": ["https://www.linkedin.com/company/python-software-foundation"],"twitter": ["https://twitter.com/ThePSF"]},"filteredOutCandidates": 0,"pagesVisited": ["https://www.python.org", "https://www.python.org/about/", "https://www.python.org/about/apps/"]},{"input": "https://apify.com","finalUrl": "https://apify.com","httpStatus": 200,"contactPageUrl": "https://apify.com/contact","contactFormUrl": "https://apify.com/contact-sales","contactFormKind": "html","emails": ["support@apify.com", "hello@apify.com", "john.wick@thecontinental.com"],"phones": [],"socials": {"linkedin": ["https://www.linkedin.com/company/apify"],"twitter": ["https://x.com/apify"]},"filteredOutCandidates": 1,"pagesVisited": ["https://apify.com", "https://apify.com/contact", "https://apify.com/contact-sales"]},{"input": "https://www.mozilla.org","finalUrl": "https://www.mozilla.org/en-US/","httpStatus": 200,"contactPageUrl": "https://www.mozilla.org/en-US/contact/","contactFormUrl": null,"emails": ["trademark-permissions@mozilla.com"],"phones": [],"socials": {"instagram": ["https://www.instagram.com/mozilla"],"linkedin": ["https://www.linkedin.com/company/mozilla-corporation"]},"filteredOutCandidates": 4,"pagesVisited": ["https://www.mozilla.org/en-US/", "https://www.mozilla.org/en-US/contact/", "https://www.mozilla.org/en-US/about/"]}]
The email on the apify.com row is taken verbatim from a page and is not verified as a working mailbox; the Actor returns what is printed on the site, nothing is guessed.
Use with Google Maps scraper (Add integration)
Get emails for every place a Google Maps scraper finds, automatically, with no copy-pasting:
- Open your Google Maps scraper (e.g.
compass/crawler-google-places) Actor or Task and go to the Integrations tab. - Click Add integration and search for website contact extractor (or "email", "contact"). Pick Contact Page & Form Finder + Website Email Extractor (
pennydata/website-contact-extractor). - Set Start when to Run succeeded.
- In the integration's input set Dataset ID to
{{resource.defaultDatasetId}}. You can also leave it empty: the finished run is passed in automatically and its default dataset is used. - Leave Field holding the website URL as
website(Google Maps scraper's field), or set another field name (url,websiteUrlanddomainare tried as fallbacks). Save.
Every finished Google Maps run now starts this Actor on its dataset. Websites are read in pages of 1,000, deduplicated by domain, and rows that already have emails are skipped. sourceTitle / sourcePlaceId in the output let you join results back to the places.
Try it in one click
Open the Actor in Apify Console with the default input (sunsethillvet.com, barefootphysiotherapy.com.au, mainstreetbakery.com) and press Start. Three domains are inside the free 25 per run, so the first run costs $0.
Use it from n8n (Google Sheet of domains to Sheet of contacts)
Low false positives: what gets filtered
Contact extractors fail most often by returning things that are not contacts. Every email candidate found on a page passes these checks before it is returned, and the number rejected is reported per domain as filteredOutCandidates:
| Candidate found in page source | Example | Result |
|---|---|---|
| Image / asset file names that look like emails | logo@2x.png, hero@3x.jpg, icons@2x.webp, font@5.0.0.woff2 | Dropped (asset-extension TLD) |
| Not a real top-level domain | x@y.invalidtld, admin@server.local, user@localhost | Dropped (TLD must be a real TLD) |
| Error-tracking and website-builder system addresses | 5d3f...@sentry-next.wixpress.com, DSN keys ...@o4504.ingest.sentry.io | Dropped (vendor domains) |
| Template placeholders | you@example.com, name@domain.com, email@email.com, john.doe@test.com | Dropped |
| Platform or CDN support addresses on a customer's page | support@godaddy.com, help@shopify.com, x@cloudfront.net, schema@schema.org | Dropped |
| Hash, UUID and long numeric ids used as a local part | 3f2504e0...c3301@..., 550e8400-e29b-...@... | Dropped |
| System mailboxes | noreply@, no-reply@, mailer-daemon@ | Dropped |
| Escaped-JSON / URL-encoded prefixes glued to a real address | u003einfo@site.com, %20billing@site.com | Prefix stripped, address kept |
Duplicates differing only in case, mailto: + visible text | INFO@Site.com and info@site.com | Merged into one |
| Obfuscated and entity-encoded addresses | sales [at] site [dot] com, info@site.de | Decoded and kept |
Addresses hidden from simple scrapers are recovered: Cloudflare email protection (data-cfemail and /cdn-cgi/l/email-protection#... links, decoded locally) and the email field of JSON-LD structured data. Phone numbers are rejected when they are all one digit, a counting sequence, a date or timestamp (2026-10-21, 21.10.2026), or an IP address. The optional MX check (validateEmails) is off by default.
The filters are heuristics. A real mailbox on a vendor-owned domain (for example a business that genuinely uses an address at a website-builder domain) can be dropped, and a placeholder that is not in the lists can slip through. Reproduce the 20-domain fixture with npm test (log: evidence/task-2019/fixture-fp-filter.log, 35 false-positive candidates filtered, 0 kept).
Honest scope
- Reads only the public homepage and up to two contact/about/impressum pages of each domain you supply (plain HTTP, no browser, no login, no proxies).
- No guessing: it never generates email patterns and never probes mail servers. It returns only what is printed on those pages (mailto links, visible addresses,
tel:links, structured data, social links). - No personal enrichment: no people search, no LinkedIn or social profile scraping, no names or job titles. If you need decision-maker emails for named people, this Actor does not do that.
- Contact forms are detected from static HTML (a non-search
<form>with a message field); forms rendered only by JavaScript or embedded from third-party iframes are not detected.contactPageUrl/contactFormUrlare null when nothing is found. - Emails rendered only by JavaScript or hidden behind contact forms are not found, so some domains legitimately return nothing (and cost nothing).
- Respects robots.txt (on by default). Blocks private/internal addresses.
- GDPR / privacy: results can include addresses that identify individuals (e.g. firstname@company.com). You are responsible for having a lawful basis to process and contact them (GDPR, ePrivacy, CAN-SPAM and similar).
Pricing
Pay per event, no subscription. $0.002 per domain with results (about $2 per 1,000 such domains).
| Event | When it is charged | Price |
|---|---|---|
domain-processed | One domain where at least one contact page, contact form, email, phone or social link was found. | $0.002 |
| (no event) | Domain with no contacts, HTTP error, DNS/timeout failure, robots.txt skip, blocked address. | $0 |
How this compares to typical bulk email finders
| This Actor | Typical bulk finder | |
|---|---|---|
| Charged when a domain returns nothing | No | Often yes (charged per domain or per run) |
Hard spend cap inside the run (maxCostUsd) | Yes, stops before exceeding | Usually only the platform-level run cap |
| Source of emails | Only what is printed on the domain's own contact pages | Varies; some guess or scrape third parties |
| Per-run summary of charged vs not charged | Yes (SUMMARY record) | Rarely |
Example input
{"urls": [{ "url": "https://apify.com" }, { "url": "https://www.python.org" }],"maxCostUsd": 0.5}
maxCostUsd is optional. With 0.009 the run stops after 3 charged domains and sets stoppedByCostGuard: true in the SUMMARY record; re-run the remaining domains later.
Reproduce the billing test locally
cd actors/website-contact-extractor && npm citest/run-local.sh # 20 real public domains, no captest/run-local.sh 0.009 # same list, stops after 3 charged domains
The script prints the SUMMARY record (charged, notCharged, failed, stoppedByCostGuard) and, per domain, whether it was BILLABLE, EMPTY (unbilled) or ERROR (unbilled). Locally Apify ignores charge calls (not pay-per-event); charged counts the calls the Actor makes.
Input
urls, startUrls, websites or domains (all aliases; list of sites/domains), datasetId (see below), maxPagesPerSite (1-10, default 3; hard limit on pages fetched per website, homepage included), maxCostUsd (optional budget cap), scrapeEmails / scrapePhones / scrapeSocials (default true; disabled fields are omitted from the output), deepScan (5 pages per site), timeoutPerUrl / timeoutSecs, respectRobotsTxt (default true), validateEmails (default false), maxConcurrency (1-20), crmCsv (hubspot default, or none).
Usage: one-click HubSpot import (contacts.csv)
Every run also writes contacts.csv to the run's key-value store (output link "HubSpot import CSV"; set crmCsv: "none" to skip). Header row: Email,First name,Last name,Phone number,Additional email addresses,Company name,Website URL, the contact property labels from HubSpot's import file guide. One row per site with an email: first email in Email, the rest ;-separated in Additional email addresses. First/last name are left empty (the actor does not guess people's names). Import via Contacts > Import > File from computer. Check consent and CAN-SPAM/GDPR rules before emailing.
Paste-compatible input
Inputs written for other website email extractors work as they are: startUrls (strings or {"url": ...}), scrapeEmails, scrapePhones, scrapeSocials, timeoutPerUrl, deepScan and maxConcurrency use the same names and meaning. Also accepted: websites, domains, followLinkPages (false = given pages only), maxContactPages (= maxPagesPerSite minus 1), datasetUrlField (= urlField); requestTimeoutSecs (= timeoutSecs); phoneRegion, defaultCountry, phoneCountryCode, proxyConfiguration and proxyFallback are accepted and ignored. Any other unknown option (retry, render, residential-proxy flags and so on) does not fail the run: it is ignored and listed in one log line.
{ "startUrls": [{"url": "https://example.org"}], "scrapeEmails": true, "scrapePhones": false, "timeoutPerUrl": 20 }
Enrich a Google Maps scraper dataset
Pass the dataset id of a finished Google Maps scraper run as datasetId. Each item's website is read, deduplicated by domain, and processed like a URL; the place title and placeId are copied to the output as sourceTitle and sourcePlaceId, so you can join results back to the places. Items without a website are skipped and cost nothing.
{ "datasetId": "YOUR_DATASET_ID", "maxPagesPerSite": 3, "maxCostUsd": 5 }
FAQ: what will a run cost?
$0.002 per website, charged only when something is found (a contact page, contact form, email, phone or social link). Websites where nothing is found, that fail to load, time out, or are blocked by robots.txt are free. Pages per site do not change the price: the charge is per website, never per page or per result. Worst case for 1,000 websites is $2.00 (every site returns something), and maxCostUsd stops the run before it passes your budget.
Switching from website-email-extractor (jurassic_jove)
Paste your existing input unchanged: urls, scrapeEmails, scrapePhones, scrapeSocials, deepScan, maxConcurrency, timeoutPerUrl have the same names here; followLinkPages: false reads only the given pages (same as crawl: false); it never follows Linktree/bio-link pages, so give it the real website URL).
{ "urls": ["https://apify.com", "https://www.python.org"], "scrapeEmails": true, "scrapePhones": true, "scrapeSocials": true, "followLinkPages": true, "deepScan": false, "maxConcurrency": 3, "timeoutPerUrl": 20 }
Price comparison, as listed on the Apify Store on 2026-10-21 (ours, jurassic_jove/website-email-extractor, prices from the public Store API pricingInfos):
| Per website with results | pennydata/website-contact-extractor | jurassic_jove/website-email-extractor |
|---|---|---|
| Until 2026-10-22 03:00 UTC | $0.002 | $0.006 |
| From 2026-10-22 03:00 UTC | $0.002 | $0.02 |
| 1,000 websites with results, from 2026-10-22 | $2.00 | $20.00 |
Differences: the rival charges only for sites where an email was found; this Actor charges for any site where a contact page, contact form, email, phone or social link was found (sites with nothing, failures and robots.txt skips are free). Linktree following is not supported here. The rival also lists a small per-run start event ($0.00005 per GB). Not affiliated with jurassic_jove.
Switching from contact-page-finder (sandy_yclept)
Paste your existing input unchanged. startUrls (strings or {"url": ...} objects) and maxContactPages (contact pages read besides the homepage) work as in that Actor; phoneRegion, proxyConfiguration and proxyFallback are accepted and ignored (this Actor uses no proxy), so they never cause an error. websites and datasetUrlField are also accepted as aliases of urls and urlField. Output field names are this Actor's own (see Output below), not identical to theirs. Price here: $0.002 per website with a result. Not affiliated with sandy_yclept.
{ "startUrls": [{ "url": "https://sunsethillvet.com" }], "maxContactPages": 3, "phoneRegion": "US", "proxyConfiguration": { "useApifyProxy": true } }
Switching from another contact extractor (summary)
Saved tasks and integrations from jurassic_jove/website-email-extractor, sandy_yclept/contact-page-finder, datagleaner/website-contact-details-scraper, maximedupre/website-emails-scraper and gordian/email-extractor can keep their input JSON: change only the Actor ID. Field names that match are used (urls, startUrls, websites, scrapeEmails, scrapePhones, scrapeSocials, maxPagesPerSite, maxContactPages, followLinkPages, crawl, datasetId, datasetUrlField, timeoutPerUrl, requestTimeoutSecs); everything else is ignored with a log line. Output field names are this Actor's own, not identical to theirs; check the Output section before swapping a pipeline. Not affiliated with any of these authors.
Migrating from gordian/email-extractor input
Paste your existing input as-is. urls (strings or {"url": ...} objects) and crawl (false = read only the given pages, no contact/about follow or probing) are supported; email is accepted and ignored. robots.txt is respected by default. Output has emails, finalUrl and pagesVisited per site.
{"urls": ["https://apify.com", {"url": "https://www.python.org"}],"crawl": false,"email": ""}
Output (one item per domain)
input, finalUrl, httpStatus, contactPageUrl, contactFormUrl (null if none), emails, emailDetails (if validated), phones, filteredOutCandidates, domain (hostname without www), socials (facebook, instagram, linkedin, twitter, youtube, tiktok, pinterest), flat arrays linkedIns, twitters, instagrams, facebooks, youtubes, tiktoks, pinterests (same links as socials, one array per platform), pagesVisited, capturedAt, or error.
Limits
Emails rendered only by JavaScript, or hidden behind contact forms, are not found. Decorative/placeholder addresses are filtered heuristically.
Non-affiliation
Not affiliated with or endorsed by any website it reads.
AI disclosure
Built with AI assistance (Claude); maintained by the publisher.
Use from Claude, Cursor or any MCP client
Apify's MCP server can expose this actor as a tool. Use this URL (format from the Apify MCP docs, ?tools= takes a comma-separated list of actors):
https://mcp.apify.com/?tools=pennydata/website-contact-extractor
Client config (mcpServers format; sign in with OAuth when the client asks):
{"mcpServers": {"apify": {"url": "https://mcp.apify.com/?tools=pennydata/website-contact-extractor"}}}
Example prompt: "Find the public contact emails and phone numbers on example.com and acme.io."
Related actors
- Bulk Email Validator (no SMTP): checks the emails this actor finds.
- Phone Number Validator (offline): checks the phone numbers this actor finds.
- Website Tech Stack Detector: adds the site's technology to the contact record.
Per-row status and run summary
Every dataset row has status: ok, error (site unreachable, HTTP error or non-public host) or skipped (disallowed by robots.txt). The key-value store record SUMMARY counts domains, ok, error, skipped, charged and notAttempted. Only ok rows with results are charged.