Website Contact Finder — Pay only for sites with contacts
Pricing
from $2.50 / 1,000 website with contacts founds
Website Contact Finder — Pay only for sites with contacts
Find emails, phones and social profiles on any list of websites. One clean row per domain, with the source page for every contact. You pay only for websites where a contact is found — blocked, dead or empty sites are free.
Pricing
from $2.50 / 1,000 website with contacts founds
Rating
0.0
(0)
Developer
Sumitr Mardy
Maintained by CommunityActor stats
1
Bookmarked
2
Total users
1
Monthly active users
2 hours ago
Last modified
Categories
Share
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, same day:
| pages billed | domains with a contact | you pay for | |
|---|---|---|---|
| per-page scraper | 336 | 72 | 336 pages |
| this actor | — | 74 | 74 contacts |
Cost per contact found, at an equal page budget:
| Free | Gold | |
|---|---|---|
| per-page scraper ($2.00 → $1.05 / 1k pages) | $0.0087 | $0.0046 |
| 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.
statusandsub_statusare never null and never empty.emails,phones,socialsandpagesare always arrays.organizationalways has all 8 string keys; unknown fields are"".checksalways has all 10 boolean keys; a check that was never reached isfalse, never null. The reason lives insub_status.
Status values
status | sub_status | meaning |
|---|---|---|
contacts_found | email_and_phone / email_only / phone_only | charged |
no_contacts | no_contact_published | pages read fine, the site publishes no address or number |
blocked | bot_challenge / http_error | a bot wall or a 4xx answered us. Worth retrying later |
unreachable | dns_error / tls_error / timeout | domain is dead, mis-configured, or too slow |
unreachable | internal_error | our fault; error says what happened |
Only contacts_found is ever charged.
How it finds things
- Homepage first. On the measured sample it carried most of the finds on its own, so most domains cost one page.
- Then the site's own contact links, then
/contact,/contact-us,/about,/about-us,/team,/impressum. - Crawling stops as soon as both an email and a phone number are found.
- 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.
- Obfuscated addresses are decoded: Cloudflare's
data-cfemail, HTML entities, andname [at] example [dot] com. - Phone numbers are normalised to E.164 with the country taken from
structured data, then the ccTLD, then your
defaultCountry. Company and VAT numbers are rejected by validation rather than by guesswork. - 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, notinfo@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-19 on Apify with the datacenter proxy on:
a contact (email or phone) : 74%an email address : 50%a phone number : 64%a social profile : 74%publishes no contact at all: 15%blocked by a bot wall : 8%dead or unreachable : 3%pages read : 3.63 per domain
Run side by side with the leading per-page scraper over the same 100 domains on the same day:
| contact (email or phone) | ||
|---|---|---|
| per-page scraper | 37 | 72 |
| this actor | 50 | 74 |
Paired, that is 18 domains where only this actor found an address against 5 the other way. Roughly a third of the remaining misses are sites that publish no address anywhere, which no scraper can fix.
Input
| field | default | notes |
|---|---|---|
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 |
concurrency | 15 | cost follows total runtime, so raising this does not raise the bill |
maxPagesPerDomain | 5 | a budget, not a target; crawling stops early on success |
timeoutMs | 8000 | a slow host is reported, not waited on |
useProxy | true | turn it off and expect more blocked rows |
allowInsecureTls | true | small business sites let certificates expire; only public pages are read and nothing is ever submitted |
defaultCountry | US | used to read local-format phone numbers when the site's country cannot be determined |
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
statusfirst:contacts_foundis the only charged status; anything else says why nothing came back. emails[].typeseparatespersonalfromgenericinboxes, and every contact carries thesource_urlit was found on, so an agent can cite it.- The MCP
call-actortool waits up to 45 seconds. A long list keeps crawling after that: fetch the results withget-dataset-itemsand the run'sdatasetId.
Legal
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.