# Contact Page Finder: Find Contact Page & Contact Form URLs (`data-done-right/contact-page-finder`) Actor

Contact page finder API: find contact page URLs and contact form pages for any website from a list of domains. Checks homepage links, common paths and the sitemap, detects the contact form, and returns public emails, phones and social links. One row per site.

- **URL**: https://apify.com/data-done-right/contact-page-finder.md
- **Developed by:** [Data Done Right](https://apify.com/data-done-right) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.64 / 1,000 sites

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
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.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## 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.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Contact Page Finder: Find Contact Page & Contact Form URLs

This **contact page finder** takes a list of domains or website URLs and returns, for each site, the **contact page URL**, the page that holds the **contact form**, and the contact details the site publishes (emails, phone numbers, LinkedIn, X, Facebook and Instagram links). It looks at the homepage links first, then common paths like `/contact` and `/kontakt`, then the sitemap, and it only reports a page after it opened it and checked that it really is a contact page. Use it to enrich lead lists, prepare outreach research, check that your own sites have a working contact page, or let an AI agent find where to write to a company.

You always get **one row per website you give**, even when nothing was found, so lists stay aligned. You pay only for sites where a contact page or contact form was found.

### Find contact page URLs for a list of websites

1. Paste domains or URLs into **Websites** (`apify.com`, `https://www.mozilla.org/en-US/`, ...). Any URL is reduced to the site root.
2. Run it. Each site is read politely: robots.txt first, usually 2 to 4 pages, 10 at most.
3. Download one row per site as JSON, CSV or Excel, or read it through the Apify API.

### What the contact page finder does

- **Three ways to find the page, cheapest first.** (1) Links on the homepage whose text or address says contact, kontakt, contacto, contato, contatti, nous contacter, get in touch and similar. (2) Common paths: `/contact`, `/contact-us`, `/about/contact`, `/get-in-touch`, `/support` and locale variants such as `/kontakt`, `/contacto`, `/contato`, `/contatti`, tried first when the homepage language matches. (3) The sitemap, from robots.txt or `/sitemap.xml`. The next step only runs when the earlier ones found nothing.
- **Nothing counts until it was checked.** A page must answer HTTP 200, must not redirect to the homepage, must not be a login wall or a "404" page served with status 200, and a guessed page must also show a contact signal (a contact title, a form, or published mailto / tel links). A site that answers 200 for every URL is therefore reported as `not_found`, not as a made-up hit.
- **Contact form finder.** It checks whether the contact page has a form with email or message fields: a plain HTML form, or an embedded Typeform, HubSpot, Jotform, Google Forms, Tally and similar widget. Newsletter, search and login forms are ignored. If the contact page has no form, up to two other candidate pages (for example `/contact-sales`) are checked and the one with the form becomes `formUrl`.
- **Public contact details, as published.** `mailto:` and `tel:` links plus visible emails and phone numbers on the homepage and contact page. Each email is marked `role` (info@, support@), `personal` (jane.doe@) or `unknown`. Obfuscations like `name [at] site [dot] com` and Cloudflare-protected addresses are decoded. Nothing is guessed or verified.
- **Social profiles.** LinkedIn, X, Facebook and Instagram profile links the site links to. Share buttons and tweet intents are ignored.
- **Redirects work.** `python.org` to `www.python.org`, `notion.so` to `notion.com`: the homepage may move to another domain once, and that domain's robots.txt is read as well.
- **Safe and polite.** robots.txt is obeyed (wildcards, `$`, Crawl-delay up to 10 s), requests are spaced out, an honest user agent is used (`ContactPageFinderBot`), no login is attempted, no CAPTCHA or bot check is bypassed, and localhost, private networks, link-local and cloud-metadata addresses are refused, also through DNS tricks and redirects.

### Input example

```json
{
  "websites": ["apify.com", "https://www.mozilla.org/en-US/", "eff.org"],
  "maxSites": 100,
  "followHomepageLinks": true,
  "checkCommonPaths": true,
  "checkSitemap": true,
  "detectForm": true,
  "extractPublicContacts": true,
  "maxRequestsPerSite": 10,
  "concurrency": 5,
  "delaySecs": 1
}
```

Only `websites` is required. Turn off `checkCommonPaths` or `checkSitemap` for fewer requests, `detectForm` to skip the form check, or `extractPublicContacts` if you only want URLs. `includePersonalEmails: false` keeps only role mailboxes and unclassified aliases.

### Output example

```json
{
  "inputIndex": 0, "input": "apify.com", "domain": "apify.com", "homepageUrl": "https://apify.com/",
  "status": "found",
  "contactPageUrl": "https://apify.com/contact",
  "alternates": ["https://apify.com/contact-sales/demo"],
  "hasContactForm": true, "formUrl": "https://apify.com/contact-sales", "formType": "html",
  "emails": [{"email": "hello@apify.com", "type": "role", "source": "mailto", "page": "https://apify.com/contact"}],
  "phones": [],
  "socialLinks": {"linkedin": "https://www.linkedin.com/company/apify", "x": "https://x.com/apify", "facebook": null, "instagram": null},
  "confidence": "high", "score": 85, "evidence": "homepage link", "checkedUrls": 3,
  "note": null, "charged": true
}
```

- `status`: `found` (a contact page or form page was found and checked), `not_found` (the site was read, no contact page we can find), `blocked` (robots.txt or a bot check stopped us), `unreachable` (DNS, connection, timeout, server error) or `invalid` (not a public website, such as an IP address or an internal name). Only `found` is charged.
- `contactPageUrl` is the best candidate. `alternates` lists other candidate URLs from links or the sitemap (not all of them were opened). `formUrl` is the page that hosts the form (not the form's action URL).
- `evidence` says how it was found: `homepage link`, `common path`, `sitemap` or `homepage form` (a single-page site whose form is on the homepage). `confidence` (`high`, `medium`, `low`) and `score` (0 to 99) come from the evidence plus the page check: a contact title, a form, published contact details.
- `checkedUrls` is the number of pages read for this site (robots.txt not counted). `note` explains why a site has no result.
- A **SUMMARY** record in the key-value store counts rows by status and the charged sites.

### Contact form finder: does the page have a form?

`hasContactForm` is `true` when the contact page, or another candidate page, has a form with an email or message field. `formType` is `html` for a normal form and `embedded:Typeform`, `embedded:HubSpot` and so on for widgets. When you turn `detectForm` off, `hasContactForm` is `null`. A form that is drawn only by JavaScript without a recognisable embed is not seen.

### Contact Page Finder API

Call it from the Apify API, Python, JavaScript, Make, Zapier or n8n. Example with the Apify Python client:

```python
from apify_client import ApifyClient

client = ApifyClient("<APIFY_TOKEN>")
run = client.actor("data-done-right/contact-page-finder").call(run_input={
    "websites": ["apify.com", "mozilla.org"], "detectForm": True})
for row in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(row["domain"], row["status"], row["contactPageUrl"], row["formUrl"])
```

### Use it in Claude, ChatGPT and Cursor

Connect Apify's MCP server (mcp.apify.com) and your AI assistant can call this contact page finder itself. For example: "find the contact page and contact form for these 20 companies and tell me which ones have a form." The main input is `websites`; the rows come back with the URLs, the form flag and the public emails and phone numbers.

### How much does it cost

Pay per event, with **no per-run start fee**: **$0.0008 per site where a contact page or contact form was found** (event `site`). `not_found`, `blocked`, `unreachable` and `invalid` sites are free, and so are duplicate domains in your list (they get a row, but are crawled and charged once). 1,000 sites that each have a contact page cost about $0.80. Paid Apify plans get 10% (Silver) to 20% (Gold and up) off. Other contact page tools on Apify charge about $0.001 to $0.005 per site. `maxSites` caps the bill, and if you set a maximum spend per run the Actor stops cleanly when it is reached and keeps the rows already delivered (rows after the limit are not delivered).

### Responsible use

- **A published address is not permission to email it.** In the EU and UK, B2B outreach needs a documented legitimate interest (GDPR Art. 6(1)(f)) and a way to object; some countries require prior consent. In the US, CAN-SPAM requires honest headers, a physical address and a working opt-out. CASL (Canada) and other laws are stricter.
- **Do not harvest personal addresses for bulk mail.** This tool is built to find where to reach a company: the contact page, the contact form and the role mailboxes the company chose to publish. Addresses that look like a person are marked `personal`, and you can leave them out with `includePersonalEmails: false`. Use the contact form or the role mailbox for first contact, keep a suppression list, and honour opt-outs.
- Only public pages are read, robots.txt is obeyed, no login is used, and the crawl is slow and small on purpose.

### Honest limits

- **JavaScript-rendered sites.** Pages are read as static HTML. A site that draws its navigation or contact form with JavaScript may show no link and no form. You then get `not_found` or a contact page with `hasContactForm: false`. A browser-based crawl is not part of this Actor.
- **Bot protection.** Sites behind Cloudflare, Akamai, AWS WAF or similar may answer 403, 429 or a challenge page. Those rows are `blocked` and free. No CAPTCHA or bot check is bypassed. A proxy can help for sites that block data-center IPs only.
- **Contact pages without the word "contact".** A page called `/reach-out` or `/say-hi` with a non-matching link text will be missed. Support and help links are listed as `alternates`; such a page becomes the result only when it has a contact form.
- **One contact page per site.** For large companies with many regions or products you get the best single candidate plus alternates, not every office.
- **Not verified.** Emails and phone numbers are shown as published. Nothing is sent, called or validated.
- **A few pages per site.** The default budget is 10 pages. Very large sites with the contact page deep in a menu or only in a sitemap index of many files may not be covered.
- **Social links** come from the homepage and contact page only. A site that links to a share button instead of its profile has no profile link.

### FAQ

#### How do I find the contact page of a website?

Add the domain to **Websites**. The Actor reads the homepage, follows the contact link if there is one, tries common paths like `/contact` and `/kontakt`, and checks the sitemap, then returns the checked URL in `contactPageUrl`.

#### How do I find a contact form URL?

Look at `formUrl` and `hasContactForm`. `formUrl` is the page that hosts the form. If the contact page itself has none, the Actor looks at up to two other candidate pages such as `/contact-sales`.

#### Does it work for non-English sites?

Yes for the usual contact words in German, Spanish, Portuguese, Italian, French, Dutch, Scandinavian languages, Polish, Czech, Turkish, Finnish, Hungarian, Russian and common Japanese, Chinese and Korean link texts. Locale paths are tried first when the page language matches.

#### Why is my site `blocked`?

Either its robots.txt disallows the page for this bot, or the server answered 401, 403 or 429, or showed a bot-check page. The Actor does not try to get around any of those, and blocked sites are free.

#### Why `not_found` when the site obviously has a contact page?

Most often the link is drawn by JavaScript, the page has a name without a contact word, or the site is a single page with an email in the footer. Add the exact URL to your own list, or look at `alternates` and `note`.

#### Are duplicate domains charged twice?

No. Each input gets its own row, but a domain is crawled once and charged once.

#### Does it send emails or fill in contact forms?

No. It only reads public pages. It never submits a form.

### Related tools

- **Work Email Finder: Find Email by Name & Domain (No SMTP)**: once you know the company, find the likely work email of a person there.
- **Google Sheets Import & Export: Apify Data, No Login**: send the rows to a Google Sheet.
- **Slack Notifications: Send Data to Slack, Discord & Email**: get an alert when a run is done.

# Actor input Schema

## `websites` (type: `array`):

Domains or website URLs to find the contact page for, one per line. Any URL is reduced to the site root. Example: apify.com, https://www.mozilla.org/en-US/, eff.org. IP addresses and internal names are rejected with a reason, and every site gets a result row.

## `maxSites` (type: `integer`):

Hard cap on how many websites from the list are processed. Extra ones are skipped. Also caps your cost.

## `followHomepageLinks` (type: `boolean`):

Read the homepage and use links whose text or address says contact, kontakt, contacto, contato, contatti, nous contacter or get in touch. This is the strongest evidence and usually needs no extra request.

## `checkCommonPaths` (type: `boolean`):

If the homepage has no usable contact link, try /contact, /contact-us, /about/contact, /get-in-touch, /support and locale variants such as /kontakt, /contacto, /contato, /contatti. A guessed page only counts if it really looks like a contact page.

## `checkSitemap` (type: `boolean`):

If nothing was found yet, read the sitemap (from robots.txt or /sitemap.xml) and look for contact page URLs. At most 3 sitemap files are read per site.

## `detectForm` (type: `boolean`):

Check whether the contact page has a form with email or message fields (a plain HTML form or an embedded Typeform, HubSpot, Jotform or Google Forms widget) and, if not, look at up to 2 other candidate pages. Newsletter, search and login forms are ignored. When off, hasContactForm is null.

## `extractPublicContacts` (type: `boolean`):

Return the email addresses and phone numbers the site publishes on its homepage and contact page (mailto and tel links and visible text), plus its LinkedIn, X, Facebook and Instagram profile links. Nothing is guessed or verified.

## `includePersonalEmails` (type: `boolean`):

Keep addresses that look like a person (jane.doe@acme.com) in the emails list, marked type personal. Turn off to keep only role mailboxes (info@, support@) and unclassified aliases. Use published addresses within GDPR and CAN-SPAM rules.

## `maxRequestsPerSite` (type: `integer`):

Upper limit of pages fetched per website: the homepage, candidate contact pages and sitemap files. robots.txt requests are not counted. Most sites need 2 to 4.

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

How many different websites are read at the same time. One website is always read one request at a time.

## `delaySecs` (type: `integer`):

Pause between two requests to the same website. A longer Crawl-delay in the site's robots.txt (up to 10 s) wins.

## `timeoutSecs` (type: `integer`):

Time allowed for one page request. Slow or unreachable sites become a row with status unreachable.

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

Optional. Send requests through Apify Proxy or your own proxies. Usually not needed. Bot protection is never bypassed: a site that blocks the request is reported as blocked.

## Actor input object example

```json
{
  "websites": [
    "apify.com",
    "mozilla.org",
    "eff.org"
  ],
  "maxSites": 10,
  "followHomepageLinks": true,
  "checkCommonPaths": true,
  "checkSitemap": true,
  "detectForm": true,
  "extractPublicContacts": true,
  "includePersonalEmails": true,
  "maxRequestsPerSite": 10,
  "concurrency": 5,
  "delaySecs": 1,
  "timeoutSecs": 10,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

## `results` (type: `string`):

One item per website you gave: status, contact page URL, contact form URL, public emails, phones and social links (all result items of this run, as JSON, CSV, Excel and more).

## `records` (type: `string`):

Key-value store records: the run summary (SUMMARY: counts by status, charged sites, spending-limit flag).

# 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 = {
    "websites": [
        "apify.com",
        "mozilla.org",
        "eff.org"
    ],
    "maxSites": 10
};

// Run the Actor and wait for it to finish
const run = await client.actor("data-done-right/contact-page-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 = {
    "websites": [
        "apify.com",
        "mozilla.org",
        "eff.org",
    ],
    "maxSites": 10,
}

# Run the Actor and wait for it to finish
run = client.actor("data-done-right/contact-page-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 '{
  "websites": [
    "apify.com",
    "mozilla.org",
    "eff.org"
  ],
  "maxSites": 10
}' |
apify call data-done-right/contact-page-finder --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,data-done-right/contact-page-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/xlzU1AWlxh4Gsi4SG/builds/bs4DS3TmZi2td6zuv/openapi.json
