Bulk Email Deliverability & SMTP Verification
Check email addresses for syntax, MX records, disposable and role-based domains, and optionally SMTP mailbox existence and catch-all behavior, with a verdict and confidence score per address.
Input schema
Input fields, types, required flags, descriptions, defaults, and prefills come from the published definition. Prefill examples are distinct from defaults.
| Field | Type | Required | Description |
|---|---|---|---|
emails | array | optional | One email address per item. You can paste a list here or use the text box below; if both are filled, the actor verifies both. Duplicates are removed case-insensitively. |
emailsText | string | optional | Paste a list separated by new lines, commas, semicolons or spaces. Useful for CSV exports and email lists. |
datasetId | string | optional | Optional Apify dataset ID. The actor reads one email per item from datasetEmailField, verifies those addresses, and can carry the original columns into each record. |
Show all input fields
| Field | Type | Required | Description |
|---|---|---|---|
emails | array | optional | One email address per item. You can paste a list here or use the text box below; if both are filled, the actor verifies both. Duplicates are removed case-insensitively. |
emailsText | string | optional | Paste a list separated by new lines, commas, semicolons or spaces. Useful for CSV exports and email lists. |
datasetId | string | optional | Optional Apify dataset ID. The actor reads one email per item from datasetEmailField, verifies those addresses, and can carry the original columns into each record. |
datasetEmailField | string | optional | Column name that holds the email address when datasetId is set. |
includeSourceFields | boolean | optional | When reading from a dataset, include the original row in sourceData so you can join the verification result back to your CRM export. |
verificationDepth | string | optional | Basic checks syntax, MX records and the A/AAAA fallback. Standard adds disposable, role-based, free-provider and domain-health checks. Deep adds a best-effort SMTP mailbox probe and catch-all detection. |
concurrency | integer | optional | How many addresses are checked in parallel. Higher is faster, but mail servers may rate-limit SMTP probes. |
maxEmails | integer | optional | Upper bound on unique addresses to verify in one run. Anything beyond this is skipped and reported in the run summary. |
maxEmailsPerDomain | integer | optional | Cap on addresses verified for the same domain. Extra addresses get an 'unknown' record explaining the cap instead of hammering one mail server. |
smtpTimeout | number | optional | Connect and response timeout for each SMTP probe. Use a lower value for fast scans and a higher value for slow mail servers. |
maxRetries | integer | optional | Extra attempts for temporary DNS, connection and SMTP 4xx failures, using exponential backoff. |
checkSmtp | boolean | optional | Run the best-effort SMTP mailbox probe even at basic or standard depth. Deep depth already includes it. |
checkCatchAll | boolean | optional | Probe a random address to detect catch-all domains. Implies SMTP probing. Deep depth already includes it. |
includeDomainHealth | boolean | optional | Add SPF, DMARC policy and common DKIM selector findings for each domain at standard or deep depth. |
Download the published input schema
View complete JSON
{
"properties": {
"checkCatchAll": {
"default": false,
"description": "Probe a random address to detect catch-all domains. Implies SMTP probing. Deep depth already includes it.",
"editor": "checkbox",
"prefill": false,
"title": "Detect Catch-all Domains",
"type": "boolean"
},
"checkSmtp": {
"default": false,
"description": "Run the best-effort SMTP mailbox probe even at basic or standard depth. Deep depth already includes it.",
"editor": "checkbox",
"prefill": false,
"title": "Check SMTP Mailbox",
"type": "boolean"
},
"concurrency": {
"default": 10,
"description": "How many addresses are checked in parallel. Higher is faster, but mail servers may rate-limit SMTP probes.",
"editor": "number",
"maximum": 50,
"minimum": 1,
"prefill": 10,
"title": "Concurrency",
"type": "integer"
},
"datasetEmailField": {
"default": "email",
"description": "Column name that holds the email address when datasetId is set.",
"editor": "textfield",
"prefill": "email",
"title": "Dataset email field",
"type": "string"
},
"datasetId": {
"description": "Optional Apify dataset ID. The actor reads one email per item from datasetEmailField, verifies those addresses, and can carry the original columns into each record.",
"editor": "textfield",
"title": "Read emails from an existing dataset",
"type": "string"
},
"emails": {
"description": "One email address per item. You can paste a list here or use the text box below; if both are filled, the actor verifies both. Duplicates are removed case-insensitively.",
"editor": "stringList",
"items": {
"type": "string"
},
"prefill": [
"test@gmail.com"
],
"title": "Email Addresses",
"type": "array"
},
"emailsText": {
"description": "Paste a list separated by new lines, commas, semicolons or spaces. Useful for CSV exports and email lists.",
"editor": "textarea",
"title": "Email Addresses (text)",
"type": "string"
},
"includeDomainHealth": {
"default": true,
"description": "Add SPF, DMARC policy and common DKIM selector findings for each domain at standard or deep depth.",
"editor": "checkbox",
"prefill": true,
"title": "Include Domain Health",
"type": "boolean"
},
"includeSourceFields": {
"default": false,
"description": "When reading from a dataset, include the original row in sourceData so you can join the verification result back to your CRM export.",
"prefill": false,
"title": "Carry source dataset columns",
"type": "boolean"
},
"maxEmails": {
"default": 10000,
"description": "Upper bound on unique addresses to verify in one run. Anything beyond this is skipped and reported in the run summary.",
"editor": "number",
"maximum": 100000,
"minimum": 1,
"prefill": 10000,
"title": "Maximum Emails",
"type": "integer"
},
"maxEmailsPerDomain": {
"default": 100,
"description": "Cap on addresses verified for the same domain. Extra addresses get an 'unknown' record explaining the cap instead of hammering one mail server.",
"editor": "number",
"maximum": 10000,
"minimum": 1,
"prefill": 100,
"title": "Maximum Emails per Domain",
"type": "integer"
},
"maxRetries": {
"default": 2,
"description": "Extra attempts for temporary DNS, connection and SMTP 4xx failures, using exponential backoff.",
"editor": "number",
"maximum": 5,
"minimum": 0,
"prefill": 2,
"title": "Max Retries",
"type": "integer"
},
"smtpTimeout": {
"default": 10,
"description": "Connect and response timeout for each SMTP probe. Use a lower value for fast scans and a higher value for slow mail servers.",
"editor": "number",
"maximum": 60,
"minimum": 1,
"prefill": 10,
"title": "SMTP Timeout (seconds)",
"type": "number"
},
"verificationDepth": {
"default": "standard",
"description": "Basic checks syntax, MX records and the A/AAAA fallback. Standard adds disposable, role-based, free-provider and domain-health checks. Deep adds a best-effort SMTP mailbox probe and catch-all detection.",
"editor": "select",
"enum": [
"basic",
"standard",
"deep"
],
"enumTitles": [
"Basic (syntax + MX)",
"Standard (+ disposable, role, free provider, domain health)",
"Deep (+ SMTP mailbox probe and catch-all)"
],
"prefill": "basic",
"title": "Verification Depth",
"type": "string"
}
},
"schemaVersion": 1,
"title": "Email Verifier Input",
"type": "object"
}
Output schema
Published result links and storage locations. This schema is not an exhaustive contract for every field in a dataset record.
| Field | Type | Required | Description |
|---|---|---|---|
results | string | optional | Dataset containing one verification record per unique email address, with only the fields that were actually measured. |
summary | string | optional | Key-value store record named SUMMARY with input, truncation, per-domain cap and verdict totals for the run. |
Download the published output schema
View complete JSON
{
"actorOutputSchemaVersion": 1,
"description": "One record per unique email address with verdict, confidence, checks that actually ran, MX and provider details, typo suggestion, alias flags, SMTP response details, catch-all confidence, domain health and a recommended next action.",
"properties": {
"results": {
"description": "Dataset containing one verification record per unique email address, with only the fields that were actually measured.",
"template": "{{links.apiDefaultDatasetUrl}}/items",
"title": "Email Verification Results",
"type": "string"
},
"summary": {
"description": "Key-value store record named SUMMARY with input, truncation, per-domain cap and verdict totals for the run.",
"template": "{{links.apiDefaultKeyValueStoreUrl}}/records/SUMMARY",
"title": "Batch Summary",
"type": "string"
}
},
"title": "Bulk Email Deliverability & SMTP Verification Output",
"type": "object"
}
Dataset schema
Published dataset views and any declared field definitions. A view can select only a subset of the returned record fields.
Download the published dataset schema
View complete JSON
{
"actorSpecification": 1,
"fields": {},
"views": {
"detailed": {
"display": {
"component": "table",
"properties": {
"aRecordFallback": {
"format": "boolean",
"label": "A/AAAA Fallback"
},
"catchAllConfidence": {
"format": "number",
"label": "Catch-all Confidence"
},
"catchAllMethod": {
"format": "text",
"label": "Catch-all Method"
},
"checks": {
"format": "object",
"label": "Checks"
},
"confidence": {
"format": "number",
"label": "Confidence"
},
"didYouMean": {
"format": "text",
"label": "Did You Mean"
},
"domainExists": {
"format": "boolean",
"label": "Domain Exists"
},
"domainHealth": {
"format": "object",
"label": "Domain Health"
},
"email": {
"format": "text",
"label": "Email"
},
"isAlias": {
"format": "boolean",
"label": "Alias"
},
"mxHost": {
"format": "text",
"label": "MX Host"
},
"mxProvider": {
"format": "text",
"label": "MX Provider"
},
"mxRecords": {
"format": "array",
"label": "MX Records"
},
"normalizedEmail": {
"format": "text",
"label": "Normalized Email"
},
"plusTag": {
"format": "boolean",
"label": "Plus Tag"
},
"reason": {
"format": "text",
"label": "Reason"
},
"recommendedAction": {
"format": "text",
"label": "Action"
},
"riskLevel": {
"format": "text",
"label": "Risk"
},
"smtpAttempted": {
"format": "boolean",
"label": "SMTP Attempted"
},
"smtpCode": {
"format": "number",
"label": "SMTP Code"
},
"smtpMessage": {
"format": "text",
"label": "SMTP Message"
},
"smtpVerifiable": {
"format": "boolean",
"label": "SMTP Verifiable"
},
"verdict": {
"format": "text",
"label": "Verdict"
},
"verifiedAt": {
"format": "text",
"label": "Verified At"
}
}
},
"title": "Detailed",
"transformation": {
"fields": [
"email",
"normalizedEmail",
"sourceData",
"verdict",
"confidence",
"riskLevel",
"recommendedAction",
"checks",
"mxHost",
"mxRecords",
"mxProvider",
"aRecordFallback",
"domainExists",
"isAlias",
"plusTag",
"didYouMean",
"smtpAttempted",
"smtpVerifiable",
"smtpCode",
"smtpMessage",
"catchAllConfidence",
"catchAllMethod",
"domainHealth",
"reason",
"verifiedAt"
]
}
},
"overview": {
"display": {
"component": "table",
"properties": {
"confidence": {
"format": "number",
"label": "Confidence"
},
"didYouMean": {
"format": "text",
"label": "Did You Mean"
},
"email": {
"format": "text",
"label": "Email"
},
"mxProvider": {
"format": "text",
"label": "MX Provider"
},
"reason": {
"format": "text",
"label": "Reason"
},
"recommendedAction": {
"format": "text",
"label": "Action"
},
"riskLevel": {
"format": "text",
"label": "Risk"
},
"verdict": {
"format": "text",
"label": "Verdict"
}
}
},
"title": "Overview",
"transformation": {
"fields": [
"email",
"verdict",
"confidence",
"riskLevel",
"recommendedAction",
"mxProvider",
"didYouMean",
"reason"
]
}
}
}
}
Key-value schema
Published key-value store definitions, when provided by this build.
This definition is not present in the default published build. See the published README for available documentation.
Published README
Read the full published documentation
Bulk Email Deliverability & SMTP Verification
Paste an email list and get one clean record per address: syntax, MX and A/AAAA routing, disposable and role-based detection, typo suggestions, SPF/DMARC/DKIM findings, a best-effort SMTP mailbox probe, catch-all detection, a risk level and a recommended next action.
Use it to clean a CRM export, screen sign-up addresses, scrub a lead list before outreach or periodically re-check stored contacts. No API keys, cookies or third-party verification service are required.
What it checks
- Syntax and normalization - catches malformed addresses and returns the normalized form plus the local part and domain.
- Domain routing - MX records sorted by priority, the primary mail host, the mail provider, and an A/AAAA fallback when a domain publishes no MX record. A null MX record (
0 .) is treated as "does not accept email", not as a deliverability win. - Typo suggestions - flags likely mistakes such as
gmial.com,hotmail.conoroutlok.comwith a corrected address indidYouMean. - Disposable, role and free providers - separate flags for throwaway domains, shared addresses such as
admin@orsupport@, and consumer providers such as Gmail or Outlook. - Alias signals -
plusTagfor+tagsubaddressing andisAliasfor plus tags and Gmail dot aliases. - Domain health - whether SPF and DMARC records exist, the DMARC policy, and which common DKIM selectors publish a usable key.
- SMTP mailbox probe - a best-effort
RCPT TOcheck against the domain's mail server with the raw code and message. - Catch-all detection - tests a random address at the domain and reports how confident the result is.
- Risk and action - a
riskLevel(low,medium,high) and arecommendedAction(send,send_with_caution,verify_manually,remove). - Batch summary - a key-value store record named
SUMMARYwith totals, truncation and per-domain cap counts. It is separate from the dataset, so one dataset row still means one unique email address.
Output per email
| Field | Type | Description |
|---|---|---|
email |
string | The normalized address, or the trimmed input when syntax fails. |
normalizedEmail |
string | Normalized address (domain lowercased, IDN converted) when syntax passes. |
localPart |
string | The part before @. |
domain |
string | The domain that was checked. |
verdict |
string | valid, invalid, risky, disposable or unknown. |
confidence |
number | 0-100 confidence score for the verdict. |
checks |
object | Only checks that actually ran: syntax, mxRecord, aRecord, smtp, disposable, roleBased, freeProvider, catchAll. |
mxHost |
string | Highest-priority mail host. |
mxRecords |
array | MX records as {host, priority} objects, sorted by priority. |
mxProvider |
string | Recognized provider such as Google, Microsoft, Proton, Zoho or Fastmail. |
aRecordFallback |
boolean | true when there is no MX record and mail routing falls back to A/AAAA. |
domainExists |
boolean | Whether DNS shows the domain exists. |
didYouMean |
string | Suggested correction for a likely typo in the domain. |
isAlias |
boolean | true for plus-tag subaddressing or a Gmail dot alias. |
plusTag |
boolean | true when the local part contains +. |
domainHealth |
object | spf and dmarc booleans, dmarcPolicy when published, and dkimSelectors for common selectors with a usable key. |
smtpAttempted |
boolean | true when an SMTP probe was attempted. |
smtpVerifiable |
boolean | true when the server returned a mailbox-level RCPT response. |
smtpCode |
number | Raw SMTP response code from the mailbox probe. |
smtpMessage |
string | Raw SMTP response text (trimmed). |
checks.catchAll |
boolean | Whether the domain accepted a random address. |
catchAllConfidence |
number | 0-100 confidence in the catch-all result. A random-address acceptance scores 60 because some servers accept first and bounce later. |
catchAllMethod |
string | Method used for catch-all detection (smtp-random-recipient). |
riskLevel |
string | low, medium or high. |
recommendedAction |
string | send, send_with_caution, verify_manually or remove. |
reason |
string | Plain-language explanation when the verdict is not a clean valid. |
verifiedAt |
string | ISO 8601 UTC timestamp. |
Fields that were not measured are omitted, not set to null. A false boolean is kept because it is a real answer.
Input
At least one of emails, emailsText, or datasetId is required. Apify's input-schema validator does not support anyOf, so this is enforced by the actor with a clear failure message.
| Field | Type | Default | Description |
|---|---|---|---|
emails |
array of strings | - | Email addresses, one per item. |
emailsText |
string | - | The same list as text, separated by new lines, commas, semicolons or spaces. |
datasetId |
string | - | Optional existing dataset ID to read emails from. The dataset must be readable by the run. |
datasetEmailField |
string | email |
Column name that holds the address when datasetId is used. |
includeSourceFields |
boolean | false |
Carry the original dataset row in sourceData. |
verificationDepth |
select | standard |
basic, standard or deep. |
concurrency |
integer | 10 |
Parallel checks (1-50). |
maxEmails |
integer | 10000 |
Upper bound on unique addresses per run (1-100000). |
maxEmailsPerDomain |
integer | 100 |
Cap per domain; extra addresses get an explanatory unknown record. |
smtpTimeout |
number | 10 |
Per-probe SMTP timeout in seconds (1-60). |
maxRetries |
integer | 2 |
Extra attempts for temporary DNS, connection and SMTP 4xx failures (0-5). |
checkSmtp |
boolean | false |
Run SMTP probing at basic or standard depth. |
checkCatchAll |
boolean | false |
Detect catch-all domains; implies SMTP probing. |
includeDomainHealth |
boolean | true |
Add SPF, DMARC and common DKIM selector findings at standard or deep depth. |
emails and emailsText are combined, then duplicates are removed case-insensitively. If both are empty the run fails with a clear message.
Example: clean a pasted list
{
"emailsText": "alice@company.com, bob@gmial.com\ncarol@mailinator.com",
"verificationDepth": "standard"
}
Example: deep check for a short list
{
"emails": ["prospect@target-company.com"],
"verificationDepth": "deep",
"checkSmtp": true,
"checkCatchAll": true,
"smtpTimeout": 15
}
Example: large list with a per-domain cap
{
"emailsText": "...",
"verificationDepth": "basic",
"maxEmails": 50000,
"maxEmailsPerDomain": 50,
"concurrency": 30
}
How to read the verdict
valid- syntax and domain routing are sound. If SMTP was enabled and answered, the mailbox was accepted. Without SMTP, this means the domain can receive mail, not that the individual mailbox exists.invalid- broken syntax, a non-existent domain, a null MX record, no MX/A routing, or a definitive mailbox rejection such as SMTP 550.disposable- the domain is on a bundled disposable-domain list.risky- the domain accepts random addresses (catch-all), so an accepted mailbox proves little.unknown- the run could not decide. Thereasonfield says why, and no guess is made in either direction.
SMTP is best-effort, by design
Port 25 is not guaranteed to be open from every network, and many large providers block probe traffic based on the sender IP's reputation, missing reverse DNS or greylisting. When the probe cannot reach the server or the server refuses the probe itself:
smtpAttemptedstaystrue.smtpVerifiableisfalse.checks.smtpis omitted.- The verdict is
unknownwhen SMTP was the deciding check.
An unreachable server is never turned into valid or invalid. This is deliberate: a blocked probe says nothing about the mailbox. If you need mailbox-level certainty for a list, run deep from a network with a reputable sending IP and expect some providers to remain unverifiable.
Catch-all detection has the same honest limit: a server that accepts every address at SMTP time may still bounce later. That is why catchAllConfidence is 60 for an accepted random address, and the verdict becomes risky rather than a confident catch-all claim.
Domain health coverage
SPF and DMARC are read from public DNS TXT records. DKIM is checked only on a small set of common selectors (google, selector1, selector2, default, k1, s1, dkim, mail). A missing dkimSelectors field means none of those selectors published a usable key; it does not prove the domain has no DKIM at all, because many domains use custom selectors. Domains with a wildcard TXT record are skipped for DKIM rather than reported as a false match.
Batch behavior and limits
- Records are pushed in batches while the run is still working, so a long list does not sit entirely in memory.
- A per-domain semaphore limits how many SMTP connections hit one mail server at once.
maxEmailstruncates the input and the count is reported in theSUMMARYrecord and status message.maxEmailsPerDomaindoes not silently drop addresses: each skipped address gets anunknownrecord with a reason.- No summary row is added to the dataset, so one row still means one unique email address.
FAQ
Is a valid verdict a guarantee that the mailbox exists? No. At standard depth it means the address is well-formed and the domain can receive mail. Only a completed SMTP probe can speak to the mailbox, and even then a provider may accept and bounce later.
Why did a Gmail address come back unknown in deep mode? Gmail and several other large providers reject probe traffic from many IP ranges. The actor reports that honestly instead of calling the address valid or invalid.
Why is checks.smtp missing instead of false? false means the server explicitly rejected the mailbox. A missing value means the probe did not produce a mailbox-level answer (blocked port, policy rejection, timeout or temporary response).
What does catchAllConfidence: 60 mean? The domain accepted a random address. That is evidence of catch-all or accept-then-bounce behavior, but it is not proof that every address is deliverable.
Can I use this for very large lists? Yes. Keep concurrency moderate, set maxEmailsPerDomain to protect individual mail servers, and use basic or standard depth unless mailbox-level probing is required.
Can I use a CSV or an existing dataset? Paste the email column into emailsText, or set datasetId to a dataset that the actor can read (attach it to the run or make it readable by ID) and choose datasetEmailField. Turn on includeSourceFields to carry the original row into sourceData.
What are the remaining limits? Outbound port 25 is unavailable from Apify cloud workers, so deep SMTP probes usually return unknown; 587/465 require authentication and are not a workaround. Provider reputation blocks, catch-all versus accept-then-bounce, catch-all mailbox resolution, arbitrary DKIM selectors, and A/AAAA parking domains cannot be resolved definitively without sending mail. Domain age is not shipped because RDAP has no service for several TLDs. These limits are reported honestly rather than converted into false verdicts.
Where is the batch summary? Open the run's key-value store and read the record named SUMMARY. It contains input counts, truncation, per-domain cap counts, verdict totals and SMTP counters. No extra row is added to the dataset.
Does it need credentials? No. MX, A/AAAA, SPF, DMARC, DKIM and SMTP checks all use public DNS and the recipient's own mail server. No account, API key or cookie is used.
Python quickstart
Use a schema-derived input and the Python standard library to start a run. An Apify account and token are required for execution.
Source build 3Q4ZEsXfpGej1x96y. Documentation is regenerated from the default published build; private drafts are not included.