# API reference — Email List Cleaner: Bulk Email Verify, Keep Qualified Emails

Developer detail for the listing page. Everything here is also in the Actor's input form and `OUTPUT` record.

## What a run writes

| Where | What |
|---|---|
| dataset | one row per qualified email (`email`, `contactRoute`, `score`, `label`, `disposition`, `verifierMethod`, `dedupeKey` …) |
| `OUTPUT` | `ok`, `mode` (`demo` / `live` / `list` / `dataset` / `estimate`), `qualified`, `rejected` per reason, `cleanFiles`, `verdict` (one address), `estimate` (with `estimateOnly`), `sourceError`, `sourceStatus`, `aiCoverage`, `timing` |
| `sendable.csv`, `risky.csv`, `bad.csv` | the cleaned list, import-ready; `OUTPUT.cleanFiles.urls` has the links |
| `REJECTED` | every dropped address with `rejectReason` and `rejectExplanation` |
| `DIGEST`, `REPORT` | the run as Markdown and as a client-ready page |

`ok` is `true` on every run that did its job — including "nothing matched" — and `false` when the source failed or
was refused: assert on it instead of counting items.

## One address in, one verdict out

```json
{"sourceMode": "actor", "emails": ["jane.doe@gmail.com"], "enableAi": false, "dedupeAcrossRuns": false, "minScore": 0}
```

`OUTPUT.verdict` is `{"email", "verdict": "send" | "send-with-care" | "skip" | "reject", "score", "verifierStatus",
"reason", "checkedAddress"}`. A rejected address carries `rejectReason` (`disposable`, `not_valid`, `role_based` …).
The built-in check answers in about a second plus the Actor start-up; it checks the domain, not the mailbox.

## n8n, Make and Zapier: verify on form submit

1. **Trigger** — your form, webhook or CRM "new contact" step.
2. **Apify — Run Actor** (n8n: *Apify* node → *Run actor*; Make: *Apify → Run an Actor*; Zapier: *Apify → Run Actor*)
   with `rich_minds/email-list-qualifier-ai` and the input above, `emails` mapped to the submitted address, wait for
   the run to finish.
3. **Apify — Get key-value store record** `OUTPUT` of the run's default store.
4. **Filter** on `verdict` = `send` (or `send-with-care`) and continue to your CRM / mailer; route `reject` to a
   "fix your address" reply using `reason`.

For lists instead of single addresses: pass `csvUrl` (a Google Sheet link) and read `sendable.csv` from the store, or
add a Webhook integration on the Actor's *Run succeeded* event.

## MCP tool description

> Verify and qualify e-mail addresses: pass `emails` (or a `csvUrl`), get each address's call (`send`,
> `send-with-care`, `skip`), a fit score against `icpDescription`, and `sendable.csv` / `risky.csv` / `bad.csv`. Set
> `estimateOnly: true` first to see the bill. One address returns `OUTPUT.verdict`.

## Estimate only

`estimateOnly: true` counts the list (`emails`, `csvUrl`, `itemsList` or the dataset's item count) and prints
`OUTPUT.estimate`: `addresses`, per tier (`basic`, `ai`) the `worstCaseUsd` (every address qualifies, capped by
`maxQualified`) and a `typicalUsd` (55 % keep rate, an estimate), `perThousandUsd`, and `deepSourceUsd` when
`ownSource` is off. Nothing is verified, charged or remembered.

## The cleaned files

| File | Holds |
|---|---|
| `sendable.csv` | delivered rows whose call is `send` |
| `risky.csv` | delivered rows with `send-with-care` / `skip`, plus addresses kept out as role, catch-all, free-mail, below `minVerifierScore` or below `minScore` |
| `bad.csv` | invalid, no result, disposable, mailbox refused |

Columns: `email, call, score, verifierStatus, verifierScore, verifierMethod, why, firstName, nextStep`, then up to 15
columns of your own file. Addresses removed by your own rules (exclude list, domain lists, already delivered) are in
none of the three; they are in `REJECTED`.

## Every other input

| Field | What it does |
|---|---|
| `minVerifierScore` (70) | Verifier score floor; the built-in check scores a clean domain 80 |
| `dedupeAcrossRuns` (`true`) | Only new addresses are verified, delivered and charged |
| `notifyEmail` | Mail the digest of new qualified emails after each run |
| `maxDiscoveryChargeUsd` (0.5) | Only with `ownSource` off: the cap on BounceVerify's bill on your account |
| `preset` | One click: five niche presets fill the ICP and rules you left empty |
| `sourceMode` | `actor` verifies `emails` / `csvUrl` live (nothing to verify = the free demo); `dataset` / `list` read rows you have |
| `fixTypos` (`true`) | Correct a probable typo (`gmial.com`) and verify it again |
| `allowDomains`, `denyDomains` | Keep only / never these domains |
| `targetFlags`, `suppressionList` | Deliver only addresses with a flag; never deliver listed ones |
| `datasetId`, `itemsList`, `emailField`, `contextFields` | Dataset or pasted rows: the address column, the fields the AI reads (`emailField` and `contextFields` are API only — hidden on the form) |
| `discoveryInput`, `autoSwitchToLive` | Raw BounceVerify input (runs the deep check); `autoSwitchToLive: false` (API only) keeps the mode you send |
| `llmProvider`, `llmModel`, `llmApiKey` | Apify model access or your own key (own-key default `google:gemini-3.5-flash-lite`) |
| `sendAlertsTo` | One field for every delivery URL — Slack, Discord, Teams, Google Chat, a Telegram bot's sendMessage link, or any other https webhook; the service is read from the address |
| `slackWebhookUrl`, `discordWebhookUrl`, `teamsWebhookUrl`, `googleChatWebhookUrl`, `telegramUrl` | API only — the same digest per service; a value here wins over `sendAlertsTo` |
| `webhookUrl`, `webhookHeaders`, `webhookSecret` | POST every qualified email to your tool; with `webhookSecret` each POST carries `X-Signature: sha256=<HMAC-SHA256 of the raw body>`; failed POSTs are listed in `OUTPUT.webhook.failedKeys` and not re-sent next run |
| `dedupeStoreName`, `dedupeTtlDays`, `historyDatasetName` | Memory per client, re-deliver after N days, running log |

## Scraper to qualifier

Clean the addresses a lead scraper just produced, with no manual export. Both recipes use the scraper's
`defaultDatasetId` (the `resource.defaultDatasetId` of its "run succeeded" webhook).

1. **Apify webhook** — on the scraper (Google Maps / LinkedIn / any Store scraper), add an integration "Run Actor"
   → `rich_minds/email-list-qualifier-ai` with this input:
   `{"sourceMode": "dataset", "datasetId": "{{resource.defaultDatasetId}}", "emailField": "email", "icpDescription": "…", "maxQualified": 200}`.
2. **Make / n8n** — a "run finished" trigger on the scraper, then "Run Actor" with the same input; read `sendable.csv`
   from the key-value store (`OUTPUT.cleanFiles.urls`) in the next step.

Each such run reports `OUTPUT.mode: "dataset"`, so you can see how many of the scraper's rows were worth sending.

## Flags



`personal_address` (a person's name, not a shared mailbox) · `business_domain` (a company domain) · `has_context` (your row has company / title / website) · `typo_corrected` · `role_based` (kept only with `allowRoleEmails`) · `catch_all` (kept only with `allowCatchAll`; deep check only) · `free_provider` · `low_verifier_score` (below 80).


## Run time per mode

| Mode | Measured time | `timeoutSecs` to pass |
|---|---|---|
| Free demo, or your own verified rows (list / dataset) | under 1 s (local run) | 120 |
| Built-in check, rules only | 0.7 s per 100 addresses on 50 domains (local run, 2026-10-04) | 300 |
| Deep check (`ownSource` off), rules only | ≈ 1 min per 100 addresses (estimate — BounceVerify: 300+ per minute) | 600 |
| AI via Apify's model access | ≈ 1–2 s per qualified email (estimate) | 1800 |
| AI with a free own key (`byok`) | a few emails per run — the AI stops after 2 minutes of rate-limit waiting, the rest rule-scored | 600 |
| AI with a paid own key | ≈ 1–2 s per qualified email (estimate) | 1800 |

Pass `timeoutSecs` of 600 + 2 s × qualified emails; `OUTPUT.timing` has the phases.


## Input fields a buyer decides on

| Field | Type | Default | What it does |
|---|---|---|---|
| `emails` | string[] | — | Addresses to verify and qualify (pasted text is searched for them); one address = one verdict in `OUTPUT.verdict` |
| `csvUrl` | string | — | A CSV / Google Sheet link instead of pasted emails; the address column is found, other columns are kept. Affects what you pay |
| `icpDescription` | string | — | Who you want to reach; the AI scores every keeper against it |
| `minScore` | 0–100 | `50` | The field that decides what you pay: below this = not delivered, not charged |
| `allowRoleEmails` / `allowCatchAll` | boolean | `false` | Keep `info@`-style and catch-all addresses (flagged, never plain `send`) |
| `maxQualified` | integer | `100` | Hard cap on output (and on what this Actor charges). Affects what you pay |
| `enableAi` | boolean | `true` | AI fit score, who-it-is and note on/off |
| `ownSource` | boolean | `true` | `true` = built-in check, any plan, no second bill · `false` = BounceVerify's deep SMTP check on your account |


## Every field of a row

| Field | Description |
|---|---|
| `itemId`, `name`, `email`, `contactRoute`, `url`, `domain` | Identity; `url` is `mailto:<email>`; stable across runs |
| `verifierStatus`, `verifierScore`, `verifierMethod`, `smtpValid`, `mxFound`, `domainExists`, `syntaxValid`, `verifiedAt` | Verifier findings, `null` when not reported; `verifierMethod` is `builtin` (domain-level) or `smtp` (mailbox) |
| `isCatchAll`, `isDisposable`, `isRoleBased`, `isFreeProvider`, `riskType`, `reason` | Risk flags and the verifier's explanation (`isCatchAll` is `null` with the built-in check) |
| `typoSuggestion`, `correctedFrom` | The correction; the mistyped original on a corrected row |
| `contextFields`, `flags`, `targetFlagsMatched` | Your row's company / title / website, rule flags, matched target flags |
| `ruleScore`, `fitScore`, `score`, `label` | Rule score, AI fit score, final score, hot / warm / cold |
| `fitReasons`, `firstName`, `seniority`, `companyType` | AI: up to 3 reasons, first name, seniority, company type (`null` when unsupported) |
| `disposition`, `dispositionNote`, `suggestedAction` | send / send-with-care / skip, a one-line note, the next step |
| `aiAssessed`, `aiModel`, `billedEvent`, `chargedEvent`, `billedAs` | Whether the AI assessed the row; pricing tier and what was charged |
| `dedupeKey`, `sourceActor`, `source`, `scrapedAt` | Provenance |
| **Canonical fields** | `url` and `score` are the fields to map; trend on `ruleScore` (same scale every run) |


## Which call runs what, and list-mode rows

| Input shape | `OUTPUT.mode` | Can it charge? |
|---|---|---|
| `{}` or the untouched form (sample rows) | `demo` | no — sample rows are `chargedEvent: demo` |
| sample rows + your own `emails` typed next to them, or `sourceMode: "actor"` + `emails` / `csvUrl` | `live` | yes — per qualified email, no second bill |
| the same with `autoSwitchToLive: false` | `demo` | no — the mode you send is the mode that runs |
| `estimateOnly: true` | `estimate` | no — counts and prices the list only |
| `sourceMode: "list"` + `itemsList`, or `"dataset"` + `datasetId` | `list` / `dataset` | yes — per qualified email; rows with a verifier result are not re-verified |

### List-mode rows — what a pasted record needs

Start from the [template](docs/img/list-template.json); a row needs only `email`:

| Field | Needed? | What it enables |
|---|---|---|
| `email` | required | the address (or the column named in `emailField`), the row identity and the dedupe key |
| `status` | optional | the qualification rules; rows without one are checked by the built-in check first |
| `score` | optional | the verifier score floor (`minVerifierScore`) |
| `smtp_valid` | optional | the mailbox-confirmed rule (else a domain-level check) |
| `is_catch_all` | optional | the catch-all rule and `riskType` (also `is_disposable`, `is_role_based`, `is_free_provider`, `risk_type`) |
| `did_you_mean` | optional | the typo fix (also `syntax_valid`, `domain_exists`, `mx_found`, `reason`, `verified_at` for the audit fields) |
| `company` | optional | `contextFields` and a better AI fit score (also `title`, `website`, `name`, or the `contextFields` you list) |


## The six ready searches

| Niche | What it looks for | Start | Measured yield |
|---|---|---|---|
| Cold-email agency | owners and heads of marketing, company domains only, verifier ≥ 80 | `preset: cold-email-agency` | not yet measured |
| SaaS SDR team | VPs and directors of sales or growth, verifier ≥ 80, fit ≥ 60 | `preset: saas-sdr` | not yet measured |
| Scraped leads cleanup | local businesses from Google Maps / directories, role mailboxes kept | `preset: scraped-leads-cleanup` | not yet measured |
| Newsletter cleanup | every real subscriber, bounces and throw-away addresses dropped | `preset: newsletter-cleanup` | not yet measured |
| Event attendee list | real people from an event or webinar, free mail kept | `preset: event-attendee-list` | not yet measured |
| Deep mailbox check | the same list through BounceVerify's SMTP check: dead mailboxes and catch-alls dropped | `preset: deep-smtp-check` | not yet measured |
