# Lead List Verifier: Phone, Website, MX, Duplicates (`verrr/lead-list-verifier`) Actor

Verify a lead list after any scraper: phones to E.164 with line type, websites checked as live, redirected, parked or dead, mail servers, duplicates and a 0 to 100 score with reasons. Keeps every original field. Reads a dataset or pasted records and is built to run as an integration after a scraper.

- **URL**: https://apify.com/verrr/lead-list-verifier.md
- **Developed by:** [VER RR](https://apify.com/verrr) (community)
- **Categories:** Lead generation, Integrations
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.50 / 1,000 lead verifieds

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

Lead List Verifier runs after any lead scraper and shows which rows are worth keeping: each record comes back with its fields intact, with its phone, website, mail servers and duplicates checked and a 0 to 100 score.

### What data you get

Every field of your record, plus one `verification` object:

- `verification.index`: the record's position in the run's input, from 0. Dataset rows come first, then pasted records.
- `verification.phone`: `e164` (the number in E.164, or null), `valid` (true or false) and `lineType` (such as `mobile`, `fixed_line`, `fixed_line_or_mobile`, `toll_free`, `voip`, or null when not known).
- `verification.domain`: the canonical registrable domain, such as `acme.co.uk` for `https://www.acme.co.uk/shop`. Null when the record has no own website.
- `verification.websiteStatus`: `live`, `redirected`, `parked`, `dead`, `no_website` or `unknown`.
- `verification.finalUrl`: where the site ended up, when it went to another domain. Otherwise null.
- `verification.mx`: true if the domain has mail servers, false if it has none, null if there is no domain or the lookup failed.
- `verification.duplicateOf`: the `index` of the first record in the output with the same domain, the same phone, or the same name and city. Otherwise null.
- `verification.score`: 0 to 100.
- `verification.reasons`: every line that moved the score, in plain words.
- `verification.checkedAt`: when the record was checked.

If a record already has a field called `verification`, it is kept as `sourceVerification` and the run log says so.

#### What each website status means

| Status | Meaning |
|---|---|
| `live` | The homepage answered with a page, on the same domain. If https failed and plain http answered, the site still reads `live`, and `reasons` names the https failure. |
| `redirected` | The site sends visitors to another domain. `finalUrl` shows where. |
| `parked` | Parking name servers, a redirect to a domain marketplace, a parking network's code in the page, or a near-empty page that offers the domain for sale or is a host's stock page. |
| `dead` | There is proof: the domain has no address in DNS, or every route to it answers 404 or 410. |
| `no_website` | The record has no website, its value is not a web address, or it is only a page on a shared platform: a social site, a directory, a booking or ordering platform, or a hosted site builder. |
| `unknown` | We could not tell. The reason is in `reasons`. |

`unknown` is not `dead`. A site that blocks us, asks for a CAPTCHA, times out, resets or refuses the connection, has an empty homepage, or is closed to us by robots.txt is `unknown`, and so is a bad certificate with no working plain http, because any of those can happen to a healthy site. A 404 on one route never overrules a failure on another. We do not retry through proxies or work around a block.

A page on a shared platform is not fetched, gets no `domain` and no `mx`, and its domain is never used to match duplicates, because two businesses on one platform have nothing to do with each other. Such a record can still match on its phone, or on its name and city.

#### How the score works

Add up the lines that apply, then keep the total between 0 and 100. There is nothing else in it, so the same checks always give the same score.

| Check | Points |
|---|---|
| Website `live` | +40 |
| Website `redirected` | +20 |
| Website `unknown` | +15 |
| Website `parked`, `dead` or `no_website` | 0 |
| Phone valid | +30 |
| Domain has mail servers | +20 |
| Mail server lookup failed | +10 |
| Has a company name | +10 |
| Duplicate of an earlier record | -30 |

A full 100 means a live website, a valid phone, mail servers and a name, with no duplicate.

### Run it after a scraper

This Actor is built to be the next step after a scraper, for example a Google Maps scraper. To chain it, open the scraper's Integrations tab, choose Add integration, pick this Actor, and make sure its Dataset field points to the dataset of the run that triggered it (the integration variable `resource.defaultDatasetId`, in double curly braces). If the Dataset field is empty the run cannot read that dataset: it stops with a message and no record is charged.

This is an independent tool. It is not made or endorsed by Apify, by Google, or by the author of any scraper it runs after.

#### Which fields are read

Names are matched ignoring case, spaces, underscores and hyphens, so `Company Name` and `company_name` are the same. Fields inside an object are read only from `company`, `business`, `organization`, `organisation` and `contactInfo`, and from `contact` if it holds no name or email. Nothing is read from an object with a person's name in it.

- Name: `company`, `companyName`, `businessName`, `employer`, `currentCompany`, `name`, `organization`, `firm`, `accountName` and similar. `title` is used only if a record has none of these and looks like a place listing, with an `address`, `street`, `placeId` or `categoryName`, because in other lists it holds a job title. A company name inside an object is read only from the first four objects above.
- Website: `website`, `homepage`, `web`, `webAddress`, `webpage`, `site`, `companyWebsite`, `companyDomain`, `domain` and similar. `url` and `link` are used only if a record has no website field at all, and only for a homepage off the shared platforms: in Google Maps output `url` is a Maps link.
- Phone: `phoneUnformatted`, `phone`, `phoneNumber`, `telephone`, `tel`, `mainPhone`, `phone1`, `phones` and similar. `mobile` and `cell` are used only if a record has no such field. A row that looks like a person's gets no phone check at all: one with a first, last or contact name, an owner, a manager, a director, or a job title such as CEO beside a company. Blank columns do not count, and `reasons` says when a phone was left unchecked. This is a pattern match on column names, so it cannot catch every case. Use this Actor on company lists, not lists of people.
- City: `city`, `town`, `locality`. Country: `countryCode`, `country`.

The run log lists which field fed each check. If no website field and no phone field can be found in the first 500 records, the run stops before charging for any record and asks for `fieldMapping`. A mapping for either one lets the run go on, and if a mapped field is not in those records the log warns.

### Input

| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
| `datasetId` | string | no | - | The dataset of leads to verify. To chain this Actor after another run, point it at that run's dataset. |
| `records` | array | no | `[]` | Leads pasted as a JSON list of objects, verified after the dataset. Each needs at least a name, website or phone field. |
| `defaultCountry` | string | no | empty | Two-letter country code for phones that have no country code and whose record has no country. |
| `fieldMapping` | object | no | `{}` | Only when detection misses a field: maps `name`, `website`, `phone`, `city` and `country` to your top-level field names. |

An empty run returns a small sample.

### Pricing

Pay-Per-Event:

| Event | Price |
|---|---|
| `lead-verified` | $1.50 per 1,000 records ($0.0015 each) |
| `apify-actor-start` | $0.00005 per run |

A record with an `unknown` website, or with a website value that is not a web address, still counts: its other fields were checked.

These records are not charged, and the `FAILED_ITEMS` record of the run's key-value store lists each one with a reason: an entry that is not an object, one with no name, website or phone, and one whose website is on a private or internal address. `FAILED_ITEMS` holds `count`, `items` (the first 10,000) and `processed`, the number of input records handled.

A run that reaches your spending limit stops there. The records after that point are not checked, not charged and not listed anywhere, and the log and status message say the run stopped.

### Sample output

```json
{
  "name": "NASA Goddard",
  "website": "https://www.nasa.gov/goddard/",
  "phone": "(301) 286-2000",
  "city": "Greenbelt",
  "countryCode": "US",
  "verification": {
    "index": 1,
    "phone": {
      "e164": "+13012862000",
      "valid": true,
      "lineType": "fixed_line_or_mobile"
    },
    "domain": "nasa.gov",
    "websiteStatus": "live",
    "finalUrl": null,
    "mx": true,
    "duplicateOf": 0,
    "score": 70,
    "reasons": [
      "+40 website is live",
      "+30 phone is valid",
      "+20 domain has mail servers",
      "+10 has a company name",
      "-30 duplicate of record 0 (same domain)"
    ],
    "checkedAt": "2026-11-05T17:26:22.097Z"
  }
}
```

### Integrations

Chain it from a scraper's Integrations tab as described above, call it from n8n, Make or Zapier, or use the Apify API:

```bash
curl -X POST "https://api.apify.com/v2/acts/VERRR~lead-list-verifier/runs?token=<YOUR_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{ "datasetId": "<dataset-id>" }'
```

### Use cases

- Clean a Google Maps lead list before it reaches your CRM: drop dead and parked businesses, and merge duplicates.
- Verify phone numbers in bulk and normalise them to E.164 for a dialler.
- Check websites in bulk for live, parked, redirected and dead domains.
- Sort any scraped lead list by a quality score, with the reason for each score.

### Limits and FAQ

- **Does it check email addresses?** No. It asks DNS whether the company's domain has mail servers. It does not validate, find or look up any mailbox, it looks up nothing about people, and it adds no personal data. Other fields of your own records pass through untouched.
- **Does a valid phone mean someone will answer?** No. `valid` and `lineType` come from each country's numbering plan, with no carrier lookup. The number fits the plan, but the line may be out of service. In the US and Canada the plan cannot tell mobile from landline, so most numbers read `fixed_line_or_mobile`.
- **Why is a phone not valid when it looks fine?** A number with no country code needs a country. Add one to the record or set `defaultCountry`. The `reasons` list says when this is the cause.
- **Why are all branches of a chain marked as duplicates?** They share a domain, so each points at the first branch. If you want every branch, ignore `duplicateOf` and add 30 back to the score.
- **Is the same site checked again for every row?** No. Each host is checked once per run, one request at a time, and each domain's mail servers once, however many rows share them.
- **Which page is checked?** The homepage on the standard ports, never the path or port in your record. JavaScript and meta refresh redirects are not followed, so such a site reads `live`.
- **Do you find every parked domain?** No. Parking pages change, and the page wording we read is English only. Parking name servers, known marketplaces and parking networks' code work in any language.
- **Is every shared platform known?** No. There is one hand-kept list of social sites, directories, booking platforms, free hosts, site builders and free mail domains, and it grows. Everything else counts as an own website, so rows on a platform we do not list yet are checked as sites, and two of them can match as duplicates by domain.
- **How big can a run be?** The largest runs tested so far had 50,000 records, with the network stubbed out: 76 seconds and 271 MB on 200 domains, 174 seconds and 690 MB on 50,000 domains. Real sites take longer. A run stops by itself at 500,000 records, which is a safety limit and not a tested size.
- **Do I need a proxy?** No, and none is used. Each site's robots.txt and crawl delay are obeyed.
- **Is it monitored?** Yes. A sample run is checked weekly.

### Legal note

This Actor reads public DNS records and public homepages for the targets you supply, and adds no personal data. Your records may hold personal data of their own: they pass through unchanged, and you are responsible for using them, and the output, in line with applicable laws and each site's terms.

# Actor input Schema

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

The dataset of leads to verify, such as the output of a scraper run. To chain this Actor after another run, set this field to {{resource.defaultDatasetId}}. Optional: leave it empty to verify pasted records only.

## `records` (type: `array`):

Leads as a JSON list of objects, verified after the dataset. Each object needs at least a name, website or phone field. An empty run returns a small sample.

## `defaultCountry` (type: `string`):

Two-letter country code for phone numbers that have no country code and whose record has no country. Left empty, such a number cannot be checked and is reported as not valid.

## `fieldMapping` (type: `object`):

Only needed when auto-detection misses a field. An object from name, website, phone, city and country to the top-level field that holds each one in your records. Any key left out is auto-detected. The run log lists what was found.

## Actor input object example

```json
{
  "records": [],
  "defaultCountry": "",
  "fieldMapping": {}
}
```

# Actor output Schema

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

One record per lead: every original field, plus verification with index, phone, domain, websiteStatus, finalUrl, mx, duplicateOf, score, reasons and checkedAt.

# 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 = {};

// Run the Actor and wait for it to finish
const run = await client.actor("verrr/lead-list-verifier").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 = {}

# Run the Actor and wait for it to finish
run = client.actor("verrr/lead-list-verifier").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 '{}' |
apify call verrr/lead-list-verifier --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,verrr/lead-list-verifier"
        }
    }
}
```

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/K11vN794a30A5BCi1/builds/C7EDtSE6cxlJE8nuY/openapi.json
