# Maps Lead Enricher: Domains to Emails & Phones (CSV) (`willed_nursery/domain-contact-enricher`) Actor

Already have a Maps export or lead CSV with websites? Get one CRM-ready row per domain: scored email, E.164 phones, socials, contact forms; your columns pass through. Bring your own domains - this Actor does not scrape Google Maps. Pay only when contacts are found.

- **URL**: https://apify.com/willed_nursery/domain-contact-enricher.md
- **Developed by:** [Joel Sumner](https://apify.com/willed_nursery) (community)
- **Categories:** Lead generation, Automation
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 92.9% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$5.00 / 1,000 domain with contacts

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/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

## Google Maps Leads Enricher — Domains/Websites to Emails & Phones (CSV)

**Already exported leads from a Google Maps scraper? Feed the website/domain column here and get one deduped, CRM-ready contact row per domain.** Scored best email, E.164 phones, social links and contact-form URLs, with all your original Maps/CSV columns passed straight through.

> This Actor does **not** scrape Google Maps. It enriches websites/domains you already have (from any Maps scraper, directory export or CRM).

### How to feed domains from a Google Maps scrape export

1. Run whichever Google Maps scraper you already use (any Apify Store Maps Actor or other tool).
2. Export the results as CSV (or a dataset JSON) and keep the column that holds the business **website** or **domain**.
3. Give it to this Actor in one of three ways:
   - **CSV URL:** put a public link to the CSV in `csvUrls`. It should have a `url`, `website` or `domain` column.
   - **Rows:** paste rows into `rows` (e.g. `{"place_id":"...","name":"...","website":"acme.com"}`). Every column comes back as `original_*`.
   - **Plain list:** paste the websites into `startUrls`.
4. Run it. You get one row per registrable domain with `bestEmail`, `emailConfidence`, `phones` (E.164), `socials`, `contactFormUrls` and your passthrough columns, ready for your CRM.

**When to use this vs. a bundled Maps + contacts tool:** if you want one tool that both scrapes Maps and grabs contacts, a bundled Maps scraper with contact enrichment may suit you better. Use this Actor when you **already have domains** (from Maps or anywhere else) and want scored emails, normalized E.164 phones, per-domain dedupe and your original columns kept, paying only for domains where contacts were found.

### GDPR / CAN-SPAM: your responsibility

- You are the data controller for any personal data you collect and use. Make sure you have a lawful basis (e.g. legitimate interest under GDPR) and honor opt-outs.
- The Actor only reads publicly visible contact details on the business websites you supply. It does not log in, bypass paywalls, or access private data.
- Any outreach must follow CAN-SPAM, GDPR/ePrivacy, CASL and local laws (accurate sender info, physical address, working unsubscribe). Results come with no guarantee of deliverability, replies, or that any email is OK to cold-email.

Give it a list of websites (startUrls, or a CSV/rows of leads). For each **registrable domain** it returns **one CRM-ready row**. Each row has:

- the best email,
- a confidence score,
- same-domain emails,
- E.164 phones,
- verified social profiles,
- contact-form URLs,
- and **all of your original CSV columns** (`original_*`).

### Why use this rather than the per-page leader (vdrmota Contact Details Scraper)

| | vdrmota | This Actor |
|---|---|---|
| Output unit | one row per page; you dedupe yourself | one row per registrable domain (www/subdomains/redirects merged) |
| Email quality | raw list | `emailConfidence` 0–1, `emailsSameDomain`, `bestEmail`, placeholder/asset/fake-TLD filtering, optional MX check |
| Sites with no public email | nothing | `contactFormUrls` (forms with an email/tel input + textarea, or a contact/mail action) |
| Lead list in → enriched list out | URLs only | CSV/rows passthrough: your columns (crm_id, company, owner…) come back on the same row |
| You pay for | every page, even empty ones | only domains where something was delivered (row pushed first, then charged) |
| Phones | tel: + text | tel: + plain text via libphonenumber-js (default region input), deduped E.164 |
| Networks | more (Threads/Snapchat/Telegram) | fewer: x, linkedin, facebook, instagram, youtube, github, tiktok (host-anchored, no share/intent/watch/issue links) |
Honest gap: vdrmota is stronger on brand, reviews (4.69★), network coverage, and DIAMOND-tier price.

### Example inputs

List of websites:

```json
{"startUrls":[{"url":"https://apify.com"},{"url":"https://stripe.com"}]}
```

Lead list enrichment (CSV passthrough — your columns come back as `original_*`):

```json
{"rows":[{"crm_id":"A1","company":"Acme","website":"acme.com"},{"crm_id":"A2","company":"Globex","website":"globex.com"}]}
```

Or point `csvUrls` at a public CSV with a url/website/domain column.

### Output sample

```json
{"domain":"acme-plumbing.com","original_crm_id":"A1","original_company":"Acme","bestEmail":"office@acme-plumbing.com","emailConfidence":0.9,
 "emailsSameDomain":["office@acme-plumbing.com"],"phones":["+15125557788"],"socials":{"facebook":["https://www.facebook.com/acmeplumbing"]},
 "contactFormUrls":["https://acme-plumbing.com/contact"],"status":"complete"}
```

(Illustrative values.)

### All input options

`startUrls` | `csvUrls` (+`csvUrlColumn`) | `rows`, `maxPagesPerDomain` (5), `maxRequestsPerCrawl` (1000), `maxConcurrency` (10), `requestTimeoutSecs` (20), `maxRequestRetries` (1), `defaultRegion` (US), `checkMx` (false; DNS `resolveMx`, cached per domain, `mxTimeoutMs` default 3000 ms; a timeout counts as unknown, not bad), `maxTotalChargeUsd` (0 = platform limit only), `proxyConfiguration` (off).

### Output

See `.actor/dataset_schema.json`, which has two views: **overview** and **full**. Every input domain gets a row, including `status: failed` with `errors`.
Rows are pushed as soon as each domain finishes. On abort or migration, rows that were not yet flushed are pushed with `aborted-partial` / `migrated-partial`.

### Pricing (pay per event)

| Event | Price |
|---|---|
| `domain-with-contacts` — a domain row with ≥1 email, phone or contact form | **$0.005** |
No per-page fee. Failed or empty domains are **free**. Set `maxTotalChargeUsd` or the run's max-charge limit to cap spend. Example: 1,000 domains with a 30% hit rate ≈ $1.50.

### Limits / ToS-safe use

Cheerio only: no JS rendering, no logins, no LinkedIn/IG profile scraping. Contact pages are found by URL keyword. Proxy is off by default.
Public contact details are still **personal data**. Under GDPR you are the controller and must have a legitimate interest; follow CAN-SPAM and local law. The tool doesn't scrape anything behind a login, and you must only submit sites you are permitted to process.

### FAQ

- **GDPR / CAN-SPAM?** Public contact details are still personal data; you are the controller. Only process sites you are permitted to, and follow local law.
- **MX check?** `checkMx: true` runs a DNS MX lookup per domain (cached, 3 s timeout; timeout = unknown, not bad).
- **JavaScript-heavy sites?** Cheerio only — no JS rendering, no logins.

### Local dev

`npm i && npm test && npm start` (input goes in storage/key_value_stores/default/INPUT.json).

### More Actors by willed_nursery

- Healthcare credentialing pair: [NPI Roster Validator](https://apify.com/willed_nursery/npi-roster-validator) ↔ [OIG LEIE Screener](https://apify.com/willed_nursery/leie-exclusion-roster-screen)
- KYB/lead: [Domain Contact Enricher](https://apify.com/willed_nursery/domain-contact-enricher)
- IP docket: [USPTO Trademark Deadlines](https://apify.com/willed_nursery/trademark-portfolio-deadline-scorer)
- Financial risk: [SEC Health Score](https://apify.com/willed_nursery/sec-financial-health-score)

# Actor input Schema

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

Public website URLs you are permitted to process. userData fields are passed through as original\_\*.

## `csvUrls` (type: `array`):

Public CSV URLs. A url/website/domain column is crawled; ALL other columns are copied into the output row as original\_<column>.

## `csvUrlColumn` (type: `string`):

Optional; auto-detects url/website/domain/site/homepage.

## `rows` (type: `array`):

Array of objects, same rules as CSV rows.

## `maxPagesPerDomain` (type: `integer`):

Maximum pages to fetch per registrable domain (contact/about/team style paths).

## `maxRequestsPerCrawl` (type: `integer`):

Hard cap on total HTTP requests for one run.

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

Max parallel page fetches.

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

Per-request timeout in seconds.

## `maxRequestRetries` (type: `integer`):

Retries after failed fetches (timeouts/5xx).

## `defaultRegion` (type: `string`):

ISO-3166-1 alpha-2 region for parsing plain-text phone numbers (e.g. US).

## `checkMx` (type: `boolean`):

OFF by default until proven on Apify platform. When on: DNS MX lookup per email domain (cached). Only ENOTFOUND/ENODATA → mx=false (−0.4). Timeouts/other DNS errors → mx=null (no score penalty). Successful MX → +0.1.

## `mxTimeoutMs` (type: `integer`):

Timeout for optional DNS MX lookups when checkMx is on.

## `maxTotalChargeUsd` (type: `integer`):

Actor stops charging and crawling once domain-with-contacts events would exceed this. The platform's own max-charge limit is also honoured.

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

Apify Proxy settings. Keep off by default — residential proxy destroys PPE margin.

## Actor input object example

```json
{
  "startUrls": [
    {
      "url": "https://apify.com"
    }
  ],
  "maxPagesPerDomain": 5,
  "maxRequestsPerCrawl": 1000,
  "maxConcurrency": 10,
  "requestTimeoutSecs": 20,
  "maxRequestRetries": 1,
  "defaultRegion": "US",
  "checkMx": false,
  "mxTimeoutMs": 3000,
  "maxTotalChargeUsd": 0,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

## `overview` (type: `string`):

No description

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

No description

# 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://apify.com"
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("willed_nursery/domain-contact-enricher").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://apify.com" }] }

# Run the Actor and wait for it to finish
run = client.actor("willed_nursery/domain-contact-enricher").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://apify.com"
    }
  ]
}' |
apify call willed_nursery/domain-contact-enricher --silent --output-dataset

```

## MCP server setup

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

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/20ZboxWzrlGlSHrdt/builds/e7VZZ74eCGcoCoziV/openapi.json
