# Website Contact Finder - Email, Phone, Contact Form (`arkayen-labs/website-contact-finder`) Actor

Find business contact details from any website: email address, phone number, or contact form. Lead enrichment that chains after Google Maps or any other scraper to add business emails to your list. Returns a reason when no contact exists, and charges only for contacts actually found.

- **URL**: https://apify.com/arkayen-labs/website-contact-finder.md
- **Developed by:** [Balaji Thukuntala](https://apify.com/arkayen-labs) (community)
- **Categories:** Lead generation, Automation, Marketing
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $15.00 / 1,000 email address founds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/platform/actors/running/actors-in-store#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Website Contact Finder - Email, Phone, Contact Form

**Find the email address, phone number or contact form for any business website. Pay only when a contact is found.**

Give it business websites from anywhere: a Google Maps scrape, a LinkedIn company
list, a CSV, or another Actor's output. For each business it returns the best
available way to get in touch.

| Route | What you get |
|---|---|
| `email` | A real address published by the business, with a confidence level |
| `form` | A link to their contact form, when they publish no address |
| `phone` | A number from a `tel:` link |
| `none` | No route, plus the reason why |

When there is no route you get a reason, and the five reasons are deliberately
distinct because they call for different decisions:

| Reason | What it means |
|---|---|
| `no-contact-published` | Site works, publishes no way to reach them |
| `blocked-by-bot-protection` | Served us a challenge page. Details may well exist |
| `site-unreachable` | Domain resolves but would not serve us. Often a block, not an outage |
| `domain-does-not-resolve` | No address record. The website is genuinely gone |
| `no-mail-server` | Domain cannot receive email at all |

The middle three would be a single blank cell in any other tool. Keeping them apart
matters: a live business that refuses datacenter traffic is still a good lead, and
reporting it as a dead website would make you discard it.

This Actor detects bot-protection challenge pages and reports them. It does not try
to defeat them.

### Why the "no route" answer matters

Every other contact scraper returns an empty cell when it finds no email, and an
empty cell tells you nothing. Roughly a quarter of businesses with no published
email address still have a working contact form. They are reachable. The empty cell
was hiding it.

And when there is genuinely no way in, the reason tells you what to do next rather
than leaving you to guess. See the table above.

### Coverage, measured and published

Nobody else in this category publishes a hit rate. Here is ours, with the sample
described so you can judge whether it resembles your list.

**Sample: 500 real businesses with websites, taken from OpenStreetMap across
Manchester, Toronto, Dublin, Hamburg and Sydney. Offices, clinics and trades.
Measured 2026-08-07. These 500 were held back and never used while building the
Actor, so this is a fair test rather than a rehearsed one.**

| Route | Share |
|---|---|
| Email address | 51.2% |
| Contact form | 12.8% |
| Phone only | 11.6% |
| No route found | 24.4% |
| **Reachable one way or another** | **75.6%** |

**Coverage depends heavily on the market, so here is the split rather than one
flattering average.** A separate 300-business sample from New York, London,
Melbourne and Berlin resolved 62%, against 75% here. Judge by the row that looks
most like your list.

| | businesses | email | form | phone | no route |
|---|---|---|---|---|---|
| `.de` | 130 | 60.8% | 12.3% | 8.5% | 18.5% |
| `.ie` | 87 | 54.0% | 9.2% | 12.6% | 24.1% |
| `.com` | 145 | 50.3% | 11.7% | 13.8% | 24.1% |
| `.co.uk` | 12 | 50.0% | 8.3% | 0% | 41.7% |
| `.com.au` | 33 | 48.5% | 18.2% | 9.1% | 24.2% |
| `.ca` | 54 | 44.4% | 20.4% | 11.1% | 24.1% |

Quality of the email half, audited by hand:

- **99.6%** of returned addresses were genuinely that business's (243 of 244, audited
  by hand on a separate 300-business sample)
- **99.1%** reproduced exactly when the same businesses were scanned again
- **66%** of the addresses in the 500-business run above were published as clickable
  `mailto:` links, which is the high-confidence tier. Set `requireHighConfidence` to
  take only those.

Contact form detection was audited separately: **95.9%** of detected forms were
genuine contact forms rather than newsletter signups, search boxes, blog comment
boxes or login forms, all of which are explicitly rejected.

**These numbers come from Western English and German speaking cities. Coverage in
other markets is not yet measured, and we would rather say so than quote you a
global figure we have not earned.**

### Pricing

You pay for outcomes, not for effort.

| Event | Price |
|---|---|
| Run starts | $0.005 |
| Email address found | $0.015 |
| Contact form or phone found | $0.005 |
| No route found | **free** |

A list of 1,000 businesses costs about **$7**, and you can work that out before you
press start. Tools that charge per page crawled cannot tell you what a run will cost
until it has finished.

Records with no route are still returned, at no charge. You paid to find out.

### Input

Three ways in. Use whichever fits.

**From another Actor.** Set `datasetId` to the upstream run's dataset. In an
Actor-to-Actor integration that is `{{resource.defaultDatasetId}}`. The website
column is detected automatically; override it with `domainField` if needed.

**From a list.** Put URLs in `startUrls` or bare domains in `domains`.

```json
{
  "domains": ["outlandish.com", "grundmann-dentistry.de"],
  "maxPagesPerSite": 6,
  "requireHighConfidence": false
}
```

### Output

```json
{
  "domain": "grundmann-dentistry.de",
  "route": "email",
  "email": "info@grundmann-dentistry.de",
  "email_type": "general",
  "confidence": "high",
  "evidence": "mailto",
  "source_url": "https://grundmann-dentistry.de/kontakt",
  "form_url": null,
  "phones": ["+493012345678"],
  "all_emails": ["info@grundmann-dentistry.de"],
  "reason": null,
  "pages_crawled": 4
}
```

`confidence` is `high` when the business published the address as a `mailto:` link
and `medium` when it was found in page text. Set `requireHighConfidence` to accept
only the former, which is worth doing for cold outreach where bounce rate matters.

`email_type` tells you which door you are knocking on, and the Actor picks in this
order:

| Value | Meaning |
|---|---|
| `personal` | A named individual, such as `j.smith@`. Best for a personalised approach |
| `general` | The main inbox: `info@`, `hello@`, `contact@` |
| `other` | A word specific to that business, such as `bookshop@` or `skin@`. Usually fine |
| `department` | A specific team: `press@`, `careers@`, `advertising@`. A real address, but often the wrong one to pitch |

Addresses that can never receive a real message, such as `noreply@` and
`postmaster@`, are never returned as the contact. Filter on `email_type` if you only
want general inboxes and named people.

### What this Actor does not claim

It does not verify that mail will actually be delivered. It tells you the business
published the address, which the 99.6% precision figure supports. Deliverability is
a different claim requiring SMTP verification, and we do not make it.

Phone numbers come from `tel:` links only. Numbers written as plain text are not
guessed at, because guessing them produces false positives.

**Phone precision is lower than email precision, and here is exactly how.** Audited
across 462 numbers from 500 businesses:

- **75.3%** come back as proper E.164 with a country code, for example `+4940364520`
- **23.8%** are returned in national format without a country code. These come from
  `.com` and `.org` sites, where the domain gives no hint about the country. We
  return the digits as published rather than prefixing a `+` and inventing a
  country: a Toronto number written as `+4169240785` reads as Swiss, and a
  confidently wrong number is worse than an obviously local one.
- **0.9%** are discarded as unusable, mostly shortcodes and public service lines.
  `116117` was being returned for two separate German practices; it is the national
  out-of-hours medical number, not either practice's line.

One known gap: a number published on several unrelated businesses, usually from a
shared template or agency, is not currently detected. It appeared on 3 of 500
businesses in testing.

# Actor input Schema

## `startUrls` (type: `array`):

Website URLs to resolve. Use this for manual runs.

## `domains` (type: `array`):

Bare domains such as example.com. Full URLs also work.

## `datasetId` (type: `string`):

Resolve every business in another Actor's output. Chain this after Google Maps Scraper, a LinkedIn company scraper, or anything else that returns a website column. In an Actor-to-Actor integration use {{resource.defaultDatasetId}}.

## `domainField` (type: `string`):

Which field in that dataset holds the website. Leave empty to detect it automatically (looks for website, url, domain, link, site).

## `maxItems` (type: `integer`):

Caps how many rows are read from the input dataset.

## `maxPagesPerSite` (type: `integer`):

How deep to look on each site. 6 is the tested default. Higher finds slightly more and costs more time.

## `requestTimeoutSecs` (type: `integer`):

How long to wait for a single page before giving up. The connect timeout is capped at 5s separately, so a host that will not answer fails fast while a slow but working page still gets its full time.

## `concurrency` (type: `integer`):

How many businesses to process at the same time. 20 is a good default. Higher finishes sooner but is harder on the sites being crawled.

## `includePhones` (type: `boolean`):

Reads tel: links only, which is precise. Phone numbers written as plain text are deliberately not guessed at.

## `requireHighConfidence` (type: `boolean`):

An email is high confidence when the site published it as a clickable mailto: link, which was 66% of finds in the published 500-business measurement. Turn this on for cold outreach where bounce rate matters. Businesses whose address appears only in page text will fall back to their contact form.

## `proxyConfiguration` (type: `object`):

Datacenter proxies are enough for most business websites and are much cheaper than residential.

## Actor input object example

```json
{
  "startUrls": [
    {
      "url": "https://outlandish.com"
    },
    {
      "url": "https://baesdesign.co.uk"
    }
  ],
  "maxItems": 100000,
  "maxPagesPerSite": 6,
  "requestTimeoutSecs": 10,
  "concurrency": 20,
  "includePhones": true,
  "requireHighConfidence": false,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

## `contacts` (type: `string`):

One record per business: the route found (email, form or phone), the contact itself, and a reason when no route exists.

## `contactsCsv` (type: `string`):

The same records as a CSV, for importing into a CRM or spreadsheet.

## `summary` (type: `string`):

Counts by route and the resolution rate for this run, so you can compare coverage against the published figures.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "startUrls": [
        {
            "url": "https://outlandish.com"
        },
        {
            "url": "https://baesdesign.co.uk"
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("arkayen-labs/website-contact-finder").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "startUrls": [
        { "url": "https://outlandish.com" },
        { "url": "https://baesdesign.co.uk" },
    ] }

# Run the Actor and wait for it to finish
run = client.actor("arkayen-labs/website-contact-finder").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "startUrls": [
    {
      "url": "https://outlandish.com"
    },
    {
      "url": "https://baesdesign.co.uk"
    }
  ]
}' |
apify call arkayen-labs/website-contact-finder --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,arkayen-labs/website-contact-finder"
        }
    }
}

```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/a9adsEQIKTczC60Qu/builds/9TItxk5MrnuVzgnUM/openapi.json
