# Contact Form Detector: Fields, CAPTCHA & Action URL (`neverempty/contact-form-detector`) Actor

Give company websites, get the contact page URL, whether it has a contact form, each field and its role (name, email, phone, message), the form action, CAPTCHA (reCAPTCHA, hCaptcha, Turnstile), honeypots, and emails and phones. Never submits a form.

- **URL**: https://apify.com/neverempty/contact-form-detector.md
- **Developed by:** [NeverEmpty](https://apify.com/neverempty) (community)
- **Categories:** Lead generation, Automation, Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.40 / 1,000 website with a contact page or forms

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 Form Detector - find the contact page and read its form fields, CAPTCHA and action URL

**For outreach teams, lead-gen agencies and form-automation builders**: give it a list of company websites and get back, for each one, **the contact page URL and the exact shape of its contact form** - every field with its name, type, label and role (`firstName`, `email`, `phone`, `company`, `message`, ...), which fields are required, where the form posts (`action`, method, encoding), which CAPTCHA is on the page (reCAPTCHA v2/v3, hCaptcha, Turnstile) and which fields are hidden spam traps.

**One row per website.** Also on the row: emails, phone numbers and social profiles printed on those pages, forms embedded from HubSpot, Typeform, Google Forms, Jotform and others, and the site's own "no sales pitches through this form" notice when it has one.

**Finds contact pages in 20+ languages** - Contact, Kontakt, Contacto, Contato, Contatti, Nous contacter, お問い合わせ, 联系我们, 문의, Связаться, İletişim, Liên hệ and more - by reading the links on the home page, then the common contact paths for the site's language.

**Reads only.** No form is ever submitted, no CAPTCHA is solved, robots.txt is followed. No API key, no login. Export as JSON, CSV or Excel.

### What you get

One row per website, with:

- **The contact page**: `contactPageUrl`, how it was found (`link`, `link-from-contact-page`, `common-path`, `given-url`) and the link text the site uses for it
- **The form**: `hasContactForm`, `contactFormUrl`, `formAction`, `formMethod`, `formEnctype`, whether the action posts to the same site, the submit button label, and the form builder behind it (`contact-form-7`, `wpforms`, `gravity-forms`, `elementor`, `hubspot`, `shopify`, ...)
- **Every field** in `formFields`: name, type, label, placeholder, required or not, the options of a select box, and a `role` you can map from
- **`fieldMap`**: role → field name, ready to fill (`{"email": "your-email", "message": "your-message"}`), and `requiredRoles`
- **What stands in the way**: `captcha` (`recaptcha-v2`, `recaptcha-v3`, `recaptcha-enterprise`, `hcaptcha`, `turnstile`, `captcha-field`), `honeypotFields`, `consentRequired` (a privacy checkbox that must be ticked), `fileUpload`
- **Forms from other services**: `embeddedForms` with the provider, its URL and how it is placed (`iframe`, `link`, `script`)
- **`noSolicitationNotice`**: the sentence where the site asks not to be sent sales messages, with the page it is on - so you can respect it
- **Other ways to reach them**: `emails`, `phones` (international format) and `phonesAsWritten`, `socials` (LinkedIn, X/Twitter, Facebook, Instagram, YouTube, TikTok, GitHub), contact pages on another domain (`offsiteContactLinks`)
- **The trail**: `pagesRead`, `httpStatus`, `status` and a plain-English `note` that says why a site has no row of results

***

### Example output (one website, socials shortened)

```json
{
  "status": "ok",
  "website": "https://wpforms.com/",
  "contactPageUrl": "https://wpforms.com/contact/",
  "contactPageFoundBy": "link",
  "contactLinkText": "contact us",
  "hasContactForm": true,
  "contactFormUrl": "https://wpforms.com/contact/",
  "formProvider": "wpforms",
  "formAction": "https://wpforms.com/contact/",
  "formActionSameSite": true,
  "formMethod": "POST",
  "formEnctype": "multipart/form-data",
  "formFields": [
    { "name": "wpforms[fields][0][first]", "type": "text",     "role": "firstName", "label": "First", "required": true },
    { "name": "wpforms[fields][0][last]",  "type": "text",     "role": "lastName",  "label": "Last",  "required": true },
    { "name": "wpforms[fields][1]",        "type": "email",    "role": "email",     "label": "Email: *", "required": true },
    { "name": "wpforms[fields][3]",        "type": "url",      "role": "website",   "label": "Website: *", "required": true },
    { "name": "wpforms[fields][2]",        "type": "textarea", "role": "message",   "label": "Comment or Message *", "required": true }
  ],
  "fieldMap": {
    "firstName": "wpforms[fields][0][first]",
    "lastName": "wpforms[fields][0][last]",
    "email": "wpforms[fields][1]",
    "website": "wpforms[fields][3]",
    "message": "wpforms[fields][2]"
  },
  "requiredRoles": ["firstName", "lastName", "email", "website", "message"],
  "captcha": [],
  "honeypotFields": [],
  "consentRequired": false,
  "fileUpload": false,
  "submitLabel": "Submit",
  "embeddedForms": [],
  "noSolicitationNotice": null,
  "socials": { "twitter": ["http://x.com/easywpforms"], "facebook": ["https://facebook.com/wpforms"] },
  "pagesRead": ["https://wpforms.com/", "https://wpforms.com/contact/"]
}
```

Field roles: `name`, `firstName`, `lastName`, `nameKana`, `email`, `phone`, `company`, `jobTitle`, `website`, `subject`, `message`, `address`, `city`, `zip`, `country`, `date`, `number`, `consent`, `file`, `captchaAnswer`, `other`. Labels in Japanese, German, French, Spanish, Portuguese, Italian and other languages are read too (`会社名` → `company`, `Nachricht` → `message`).

### Input

- **Websites** (`domains`) - domains (`example.com`) or URLs, one per line, up to 500 per run. Give a page URL (`https://example.com/de/kontakt`) and that page is read first.
- **Pages read per website** (`maxPagesPerWebsite`, 1 to 8, default 4) - the home page counts as one. Reading stops as soon as a contact form is found.
- **Websites read at the same time** (`maxConcurrency`, 1 to 8, default 4).
- **Time limit per request** (`requestTimeoutSecs`, 5 to 60 seconds, default 20). Busy servers (HTTP 429 and 5xx) are asked again up to two more times.

Leave the list empty and the example site contactform7.com is read, free of charge.

### When a website has no row of results

Every website you give gets a row, so you can see what happened. `status` says why:

| status | what it means |
|---|---|
| `ok` | a contact page or a contact form was found |
| `no-contact-page-found` | the pages read have no contact link and the usual contact paths do not answer with a contact page |
| `robots-disallowed` / `robots-unreachable` | the site's robots.txt does not allow the page, or robots.txt itself could not be read - the page is not fetched |
| `login-required` | the site sends visitors to a login page |
| `blocked` | the site answers with a bot check or refuses the request |
| `not-found`, `http-error`, `unreachable`, `unreadable`, `not-html` | the site is gone, answers with an error, cannot be reached, or is not a web page |
| `bad-input` | not a website (an email address, an ftp link, a private address) |

Only `ok` rows are charged.

### Things this Actor is careful about

- **It never submits a form and never solves a CAPTCHA.** It reports what is there.
- **robots.txt is followed** for every page, and a site whose robots.txt cannot be read is left alone.
- **A page is called a contact page only when it is one.** A path that answers 200 for anything (`/contact/` on a site that returns its home page for every URL) is checked for contact wording and a form before it is used. Links that lead to another company's site are not followed.
- **Spam traps are named, not filled.** Hidden fields named like a trap (`website_hp`, `honeypot`, `ak_hp_textarea`, ...) are listed in `honeypotFields` so your own tooling can leave them empty.
- **Search boxes, newsletter sign-ups and login forms are not counted** as contact forms.
- **The same website given twice is read and charged once**, and a run restarted by the platform does not charge again for websites already returned.

### Good to know

- Forms that a script draws after the page loads are not in the page's HTML. When the contact page loads a form script (HubSpot, Marketo, Pardot, ...) or has almost no text without JavaScript, `jsFormHint` says so and `embeddedForms` lists the script.
- Some contact pages carry only an email address or a phone number. Those rows have `hasContactForm: false` and the address and number filled in.
- A big site can have several forms (sales, support, press). The main one is in `formFields`; `otherContactForms` counts the rest.

### Frequently asked

**How do I find the contact form of a list of company websites?**
Paste the domains into Websites and run. Each row gives the contact page URL, the form's fields with their roles, and where it posts.

**How do I know which fields a contact form requires?**
`requiredRoles` lists the roles of the required fields, and each entry of `formFields` has `required: true` or `false`.

**Which websites use reCAPTCHA, hCaptcha or Cloudflare Turnstile on their contact form?**
The `captcha` column names the kind found on the page with the form. An empty list means no CAPTCHA mark was found in the HTML.

**Can it find contact pages on German, Japanese or Spanish sites?**
Yes. Links such as Kontakt, お問い合わせ, Contacto, Contato and Contatti are recognised, and the usual contact path for the site's language is tried when no link is found.

**Does it send messages through the forms?**
No. It reads the form and returns its shape. Sending is up to you, and `noSolicitationNotice` shows when a site asks not to receive sales messages.

**Does it need a browser or an API key?**
No API key and no login. Pages are read as plain HTML, which keeps runs fast and inexpensive.

### Limitations

- Fields that only exist after JavaScript runs are not read (see `jsFormHint`).
- Sites behind a bot check, a login, or a robots.txt rule are reported with their `status` and not read.
- Up to 8 pages per website are read; a form hidden deeper is not found.
- CAPTCHA detection is from marks in the HTML; a CAPTCHA added only after a click is not seen.

### Pricing

You pay per website where a contact page or a contact form was found (`status: ok`). Websites with nothing found, errors, blocked sites and the free example run are not charged. Set a maximum charge for a run and the Actor stops before going over it, and says how many websites were left out.

### Thanks for using this Actor

Found a site it reads wrong, or a field role you need? Open an issue on the Actor's page - fixes for reported sites come first.

# Actor input Schema

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

Company websites to check, one per line (1 to 500 per run): a domain (example.com) or a URL. If you give a page address (for example https://example.com/de/kontakt), that page is read first. The home page is read, then the links that look like a contact page in any of 20+ languages (Contact, Kontakt, Contacto, Contatti, お問い合わせ, ...), following the site's robots.txt. No form is ever submitted and no CAPTCHA is solved. The same website given twice is read and charged once. If this is empty, the example site contactform7.com is read and nothing is charged.

## `maxPagesPerWebsite` (type: `integer`):

Most pages read for one website, the home page included (1 to 8). Reading stops as soon as a contact form is found. More pages find more forms on sites that hide the form one click behind the contact page; fewer pages make a run faster.

## `maxConcurrency` (type: `integer`):

How many websites are read in parallel (1 to 8).

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

How long to wait for one page to answer (5 to 60 seconds). HTTP 429, 500, 502, 503 and 504 are asked again up to two more times.

## Actor input object example

```json
{
  "domains": [
    "contactform7.com",
    "wpforms.com",
    "sunsethillvet.com"
  ],
  "maxPagesPerWebsite": 4,
  "maxConcurrency": 4,
  "requestTimeoutSecs": 20
}
```

# Actor output Schema

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

One row per website: the contact page URL and how it was found, whether a contact form is on it, the form fields with their roles, the form action, method and provider, CAPTCHA and honeypot fields, embedded form providers, a 'no sales pitches' notice if the page states one, and the emails, phones and social profiles on the page. A website that could not be read, or where no contact page was found, comes back as a free row that says why.

# 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 = {
    "domains": [
        "contactform7.com",
        "wpforms.com",
        "sunsethillvet.com"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("neverempty/contact-form-detector").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 = { "domains": [
        "contactform7.com",
        "wpforms.com",
        "sunsethillvet.com",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("neverempty/contact-form-detector").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 '{
  "domains": [
    "contactform7.com",
    "wpforms.com",
    "sunsethillvet.com"
  ]
}' |
apify call neverempty/contact-form-detector --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,neverempty/contact-form-detector"
        }
    }
}
```

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/S8xVwljk6uj1mmhDZ/builds/Cq7mVFKIbGJufWlsl/openapi.json
