# List Cleaner & Normalizer (`apex-nexus/list-cleaner-normalizer`) Actor

Free email list cleaner & email list dedupe / contact list normalizer & mailing list dedupe tool: paste JSON/CSV contact lists, deduplicate, validate emails & domains, download clean rows plus a reject report. B2B list hygiene — free when empty.

- **URL**: https://apify.com/apex-nexus/list-cleaner-normalizer.md
- **Developed by:** [Valunex](https://apify.com/apex-nexus) (community)
- **Categories:** Business, Marketing
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.0001 / actor start

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

## List Cleaner & Normalizer — free email list cleaner, email list dedupe & contact list normalizer / mailing list dedupe tool

Paste a raw list of names, emails, domains and phones (JSON or CSV text) and
get back a **clean, deduplicated, normalized dataset plus a reject report**.
This Actor is a free **email list cleaner** / **email list dedupe** /
**list validation free** / **contact list normalizer** / **mailing list dedupe tool** /
**B2B list cleaner** for sales ops teams — no scraping, no SMTP probing, no per-email fees.

**Actor:** `apex-nexus~list-cleaner-normalizer`
**Store listing:** FREE — email list cleaner & contact list normalizer

### What it is for

Bring your own data — paste messy prospect, vendor or contact lists as a JSON
array of objects or CSV text with a header row (Apify Actor input has no file
upload, so pasted text is the input). See [Input](#input) for the full schema
and [Output](#output) for the returned shapes. The Actor:

1. **Parses** the pasted input (JSON or CSV).
2. **Normalizes** every field: trimmed, whitespace-collapsed; emails and
   domains lowercased; domains checked as IDNA-valid.
3. **Deduplicates** rows by normalized email (or, when a row has no email, by
   the name + domain + phone signature) — first occurrence wins.
4. **Validates** email syntax plus domain/MX format (best-effort DNS: a hard
   domain failure rejects the row, transient DNS problems keep it).
5. **Strips** invalid rows and returns the clean rows plus one reject report
   with counts, reason codes and the rejected rows.

Typical workflows:

- **Sales list cleanup.** Paste a messy prospect export; download clean CSV
  with valid emails only, plus the reject report showing exactly which rows
  were dropped and why.
- **B2B list dedupe.** De-dupe by email across merged lists while keeping
  domain-only and name-only contact fragments.
- **Mailing list hygiene.** Run before a campaign to strip invalid addresses
  and typos (`not-an-email`, empty rows) without paying per-email validation
  fees.
- **Domain list normalizer.** Lowercase and de-dupe domain lists, validate
  that each domain resolves.

### What this is NOT

This is explicitly **not a tracker**: no watchlist state, no delta alerts,
nothing remembered between runs. Each run is stateless and independent. It is
also **not an SMTP/MX email validator**: it does not probe mail servers, does
not check mailbox existence, and does not use paid enrichment APIs — the
domain check is DNS format/resolution only, so it is safe on any list. The
Actor is free when there is nothing to return (empty input → empty dataset).

### Data sources

User-supplied data only. The Actor never fetches third-party data — the only
external lookups are best-effort DNS MX/A resolution of email domains (public
DNS, no credentials, no proxies, no API keys, no LLMs).

### Search terms (Store discovery)

Users find this Actor on the Apify Store with terms such as:

- email list cleaner
- list validation free
- domain list normalizer
- mailing list dedupe tool
- B2B list cleaner
- contact list normalizer
- email list dedupe

The Store title and description carry the core terms ("List Cleaner &
Normalizer", "email list cleaner", "contact list normalizer", "mailing list
dedupe tool", "reject report"); this section lists them for reference when
updating the listing metadata.

### Input

See `.actor/input_schema.json`. All fields are optional; the default run
cleans the built-in mixed list (names, emails, domains, phones with
duplicates and invalid rows).

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `inputText` | string (textarea) | built-in mixed list | Pasted list: JSON array of objects (`name`, `email`, `domain`, `phone`, …) or CSV text with a header row. |
| `normalizeDomains` | boolean | `true` | Lowercase emails/domains; trim and collapse whitespace on every field. |
| `rejectInvalidEmails` | boolean | `true` | Drop rows whose email fails syntax validation or whose domain publishes no MX and resolves to no A/AAAA record. |
| `maxRejectedReported` | integer | `500` | Cap on rejected rows embedded in the reject report (counts stay exact), 1–5000. |

Example input (default run):

```json
[
  {"name": "Alice Smith", "email": "ALICE@Example.COM ", "domain": "Example.COM", "phone": "+1 (555) 0100"},
  {"name": "Bob Jones", "email": "bob@example.com", "domain": "example.com", "phone": "555-0100"},
  {"name": "Carol Davis", "email": "not-an-email", "domain": "example.com", "phone": "555-0200"},
  {"name": "Frank Miller", "email": "FRANK@Example.COM", "domain": "Example.COM", "phone": "+1-555-0100"},
  {"name": "Grace Lee", "email": "", "domain": "example.com", "phone": "555-0500"}
]
```

Example input (CSV):

```csv
name,email,domain,phone
Alice Smith,ALICE@Example.COM,Example.COM,+1 (555) 0100
Carol Davis,not-an-email,example.com,555-0200
```

### Output

One `clean_record` dataset item per valid row, plus exactly one
`reject_report` item (last item). Download the whole dataset as CSV or JSON
from the Apify run — clean CSV and the reject report together.
See `.actor/output_schema.json` and `.actor/dataset_schema.json`.

| Field | Type | Description |
|-------|------|-------------|
| `_type` | string | `"clean_record"` or `"reject_report"` |
| `rowId` | integer | Sequential id of the clean row (`clean_record`) |
| `name` | string | Normalized name |
| `email` | string | Normalized email (lowercased) |
| `domain` | string | Normalized domain (lowercased, IDNA-valid) |
| `phone` | string | Normalized phone |
| `normalizedAt` | string | UTC timestamp (ISO 8601) |
| `inputRows` / `cleanRows` / `rejectedRows` | integer | Counts (`reject_report`) |
| `reasons` | object | Rejection count per reason code (`reject_report`) |
| `rejected` | array | Rejected rows with row index + reason (`reject_report`) |
| `generatedAt` | string | UTC timestamp (ISO 8601, `reject_report`) |

Reject reason codes: `invalid-email`, `invalid-domain`,
`domain-not-resolvable`, `duplicate`, `empty-row`.

Example output item (clean record):

```json
{
  "_type": "clean_record",
  "rowId": 1,
  "name": "Alice Smith",
  "email": "alice@example.com",
  "domain": "example.com",
  "phone": "+1 (555) 0100",
  "normalizedAt": "2026-11-03T12:00:00+00:00"
}
```

Example output item (reject report):

```json
{
  "_type": "reject_report",
  "inputRows": 10,
  "cleanRows": 7,
  "rejectedRows": 3,
  "reasons": {
    "invalid-email": 1,
    "domain-not-resolvable": 1,
    "empty-row": 1
  },
  "rejected": [
    {"rowIndex": 3, "row": {"name": "Carol Davis", "email": "not-an-email", ...}, "reason": "invalid-email"},
    {"rowIndex": 4, "row": {"name": "Dave Wilson", ...}, "reason": "domain-not-resolvable"},
    {"rowIndex": 7, "row": {"name": "", "email": "", ...}, "reason": "empty-row"}
  ],
  "generatedAt": "2026-11-03T12:00:00+00:00"
}
```

### Limits

- One pasted list per run (JSON or CSV text; no file upload).
- Default mixed list (10 rows, duplicates + invalid rows) → 7 clean rows +
  reject report in well under 5 minutes.
- DNS domain checks are best-effort with a short timeout; transient DNS
  problems keep the row instead of rejecting it.
- Invalid input finishes with an empty clean dataset plus a reject report
  rather than crashing.
- Deterministic for a given input: same list in, same clean rows out (apart
  from timestamps).

### Example runs

**Default run — built-in mixed list:**

```json
{}
```

**CSV paste, keep everything (no domain validation):**

```json
{
  "inputText": "name,email\nA,a@x.com\nB,bad-email\n",
  "rejectInvalidEmails": false
}
```

**JSON paste, large reject lists:**

```json
{
  "inputText": "[{\"email\": \"a@x.com\"}, {\"email\": \"a@x.com\"}]",
  "maxRejectedReported": 100
}
```

### Quality gate (live, verified)

**Actor:** `apex-nexus~list-cleaner-normalizer` (actor id `JzALhGjT7HhhQCghP`,
private, not published — publication is a director action).
**Build:** `1.0.1` (Apify build id `IaBMBiPQ1MYpg049c`, status SUCCEEDED,
tagged `latest` via `apify push -b latest`). Apify validated the input/output
schemas during the build.

**Default run** (input `{}`, built-in 10-row mixed list with duplicates and
invalid rows): run id `WITAafDLwIoTfKxZJ`, status SUCCEEDED, exit code 0,
`runTimeSecs` 3.134 (< 5 min ✓), default dataset `nlMYVJdTvIPvcDSAy`.

Output: **7 clean rows** (Alice Smith, Bob Jones, Eve Wong, Frank Miller,
Grace Lee, Hank Boone, Heidi Cruz — emails lowercased/deduped; `ALICE@...`
and `FRANK@...` kept as distinct addresses) plus **1 reject report**
(10 input rows, 3 rejected):

- `invalid-email`: 1 — `Carol Davis / not-an-email`
- `domain-not-resolvable`: 1 — `Dave Wilson / no-such-domain-9x9.invalid` (live DNS NXDOMAIN)
- `empty-row`: 1 — fully empty row

Competing email list validators on the Apify Store are PAY_PER_EVENT
per-address SMTP/MX checkers; this Actor is free, self-service, BYO-data list
hygiene.

### Claims statement

All claims in this README are limited to what the product verifiably does:
it parses user-supplied JSON/CSV contact lists, normalizes and deduplicates
them, validates email syntax plus domain/MX format via best-effort DNS, and
returns clean rows with a reject report. This README contains no revenue,
user, or adoption claims, because none are verified.

# Actor input Schema

## `inputText` (type: `string`):

Pasted list: a JSON array of objects (name, email, domain, phone, ...) or CSV text with a header row. Default is a small mixed list with duplicates and invalid rows.

## `normalizeDomains` (type: `boolean`):

Lowercase emails and domains, trim and collapse whitespace on every field. On by default.

## `rejectInvalidEmails` (type: `boolean`):

Drop rows whose email fails syntax validation or whose domain publishes no MX record and resolves to no A/AAAA record. Rows without an email are kept when they carry another identifier (name, domain or phone).

## `maxRejectedReported` (type: `integer`):

Cap on rejected rows embedded in the reject-report item (rejection counts are always exact).

## Actor input object example

```json
{
  "inputText": "[{\"name\":\"Alice Smith\",\"email\":\"ALICE@Example.COM \",\"domain\":\"Example.COM\",\"phone\":\"+1 (555) 0100\"},{\"name\":\"Bob Jones\",\"email\":\"bob@example.com\",\"domain\":\"example.com\",\"phone\":\"555-0100\"},{\"name\":\"Carol Davis\",\"email\":\"not-an-email\",\"domain\":\"example.com\",\"phone\":\"555-0200\"},{\"name\":\"Dave Wilson\",\"email\":\"dave@no-such-domain-9x9.invalid\",\"domain\":\"no-such-domain-9x9.invalid\",\"phone\":\"555-0300\"},{\"name\":\"Eve Wong\",\"email\":\"eve@example.com\",\"domain\":\"example.com\",\"phone\":\"555-0400\"},{\"name\":\"Frank Miller\",\"email\":\"FRANK@Example.COM\",\"domain\":\"Example.COM\",\"phone\":\"+1-555-0100\"},{\"name\":\"\",\"email\":\"\",\"domain\":\"\",\"phone\":\"\"},{\"name\":\"Grace Lee\",\"email\":\"\",\"domain\":\"example.com\",\"phone\":\"555-0500\"},{\"name\":\"Hank Boone\",\"email\":\"\",\"domain\":\"\",\"phone\":\"\"},{\"name\":\"Heidi Cruz\",\"email\":\"HEIDI@Gmail.COM\",\"domain\":\"gmail.com\",\"phone\":\"555-0700\"}]",
  "normalizeDomains": true,
  "rejectInvalidEmails": true,
  "maxRejectedReported": 500
}
```

# Actor output Schema

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

Clean rows plus one reject-report item.

# 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("apex-nexus/list-cleaner-normalizer").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("apex-nexus/list-cleaner-normalizer").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 apex-nexus/list-cleaner-normalizer --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,apex-nexus/list-cleaner-normalizer"
        }
    }
}
```

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/JzALhGjT7HhhQCghP/builds/MgcWhGkuIB2qmyE2E/openapi.json
