DEV Community

PennyData
PennyData

Posted on Edited on

Validate a CSV of emails without SMTP probing: syntax, MX, disposable and role checks via API

What it checks (and what it does not)

For each address the actor checks: syntax, DNS (MX records, A/AAAA fallback, null-MX), disposable domain, role account (info@, admin@, ...), free provider (gmail etc.) and gives a typo suggestion (gmial.com -> gmail.com).

When NOT to use it: it never contacts a mail server, so it cannot tell you whether jane@company.com has a real mailbox. A row can be valid: true and still bounce. If you need mailbox-level confirmation you need an SMTP-probing service (which comes with blocked ports, greylisting and sender-reputation costs). This tool is for the cheap first pass that removes typos, dead domains, throwaway domains and role addresses before you send or pay for deeper checks.

Input

Field names come straight from the actor's input schema: emails (array), csvUrl (public CSV or Google Sheets link, email column auto-detected), csvColumn (optional header name), inputDatasetId, emailField, maxEmails (default 10000), maxCostUsd (hard spend cap) and concurrency (default 10).

{
  "csvUrl": "https://example.com/my-list.csv",
  "csvColumn": "email",
  "maxEmails": 5000,
  "maxCostUsd": 2,
  "concurrency": 10
}
Enter fullscreen mode Exit fullscreen mode

Call it with curl

Get a token from your Apify account settings. This runs synchronously and returns the rows:

curl -X POST "https://api.apify.com/v2/acts/pennydata~bulk-email-validator-no-smtp/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"emails":["jane@gmail.com","info@example.com","bob@mailinator.com","x@gmial.com","not-an-email"],"maxCostUsd":1}'
Enter fullscreen mode Exit fullscreen mode

Call it from Node.js

import { ApifyClient } from 'apify-client';

const client = new ApifyClient({ token: process.env.APIFY_TOKEN });

const run = await client.actor('pennydata/bulk-email-validator-no-smtp').call({
  csvUrl: 'https://example.com/my-list.csv',
  csvColumn: 'email',
  maxCostUsd: 2,
});

const { items } = await client.dataset(run.defaultDatasetId).listItems();
const keep = items.filter(r => r.valid && !r.isDisposable && !r.isRole);
console.log(`${keep.length} of ${items.length} addresses kept`);
Enter fullscreen mode Exit fullscreen mode

I have not run these exact snippets end to end for this post; the field names are copied from the schema, so check them against the actor's Input tab if something errors.

Output rows

Illustrative sample of the shape (from the actor README, a disposable address):

{
  "input": "bob@mailinator.com",
  "email": "bob@mailinator.com",
  "valid": false,
  "score": 20,
  "riskType": "disposable",
  "status": "disposable",
  "reason": "disposable_domain",
  "domain": "mailinator.com",
  "hasMx": true,
  "isDisposable": true,
  "isRole": false,
  "isFreeProvider": false,
  "didYouMean": null
}
Enter fullscreen mode Exit fullscreen mode

Cost

Pricing is pay-per-event: one email-validated event per checked address, no subscription and no compute fees on top. Rows whose DNS lookup fails (status unknown) are not billed, and maxCostUsd stops the run at your cap. For 1,000 addresses, multiply the per-event price by 1,000. See the actor page for current pay-per-event pricing: https://apify.com/pennydata/bulk-email-validator-no-smtp

Drop-in note

The actor also accepts the aliases csvFile (for csvUrl) and emailList (for emails), so inputs you saved for other email-validator actors usually run without edits.

I publish the Apify actor used above (pennydata/bulk-email-validator-no-smtp) and earn from its runs. Written with AI assistance (Claude).

Top comments (0)