Skip to main content
Thirdwatchthirdwatch
Companies & leads

Verify Email Lists in Bulk to Cut Your Bounce Rate (2026)

Run syntax, MX, live SMTP mailbox, catch-all, disposable, role, free-provider, and optional SPF/DKIM/DMARC checks with a bulk email verifier on Apify.

Editorial illustration for companies & leads
Jun 20, 2026 · 5 min read · 1,051 words
View the Apify scraper →

Thirdwatch's Email Verifier & Validator cleans email lists with syntax, MX/A fallback, non-sending SMTP mailbox checks, catch-all detection, disposable/role/free-provider flags, and optional SPF/DKIM/DMARC health. It returns an integration-friendly good / risky / bad status, a technical status, a 0–100 score, and the evidence behind each decision. No separate verifier account or API key is required.

Why verify an email list before a campaign?

A malformed address or dead domain is easy to reject. The harder—and more valuable—question is whether the recipient mail server recognizes one exact mailbox. That requires an SMTP conversation, not just an MX lookup.

The Email Verifier & Validator performs both layers. It first removes deterministic failures such as bad syntax, missing mail infrastructure, and disposable providers. It then opens a connection to the receiving mail server, submits an SMTP envelope recipient check, and closes the connection without sending a message. If the server rejects the mailbox with a definitive 5xx response, the result is bad. If it accepts the mailbox, the Actor runs a randomized control probe to detect catch-all behavior.

This distinction matters for growth and RevOps teams. A domain with valid MX records may still contain a misspelled or disabled mailbox. Conversely, a provider may deliberately hide recipient status or temporarily throttle verification traffic. A useful verifier preserves that uncertainty instead of turning every reachable domain into a false “valid.”

How does it compare with the alternatives?

Approach Mailbox check Catch-all Setup Ongoing cost
Regex + DNS script No No You build and maintain it Engineering time
Verification SaaS Yes Usually Separate account, credits, API key Subscription or credits
Thirdwatch Email Verifier Yes Yes One Apify Actor call From $0.50/1K results

The Actor uses Apify's authenticated proxy as an HTTP CONNECT tunnel to the recipient MX server. That matters because cloud platforms commonly restrict direct outbound SMTP. The result is live mailbox-level evidence without a hidden per-email verification vendor, while DNS caching and 256 MB execution keep the economics suitable for bulk lists.

How to verify a batch

Set your Apify token, then pass an emails array. SMTP and catch-all checks are enabled by default; SPF/DKIM/DMARC auditing is optional because it adds DNS work that most list-cleaning jobs do not need.

import os
import pandas as pd
import requests

response = requests.post(
    "https://api.apify.com/v2/acts/thirdwatch~email-verifier/run-sync-get-dataset-items",
    params={"token": os.environ["APIFY_TOKEN"]},
    json={
        "emails": [
            "person@company.com",
            "sales@company.com",
            "person@gmail.com",
            "hello@mailinator.com",
            "not-an-email",
        ],
        "smtpCheck": True,
        "detectCatchAll": True,
        "checkDomainHealth": False,
        "maxConcurrency": 10,
        "smtpTimeoutSecs": 4,
    },
    timeout=900,
)
response.raise_for_status()
df = pd.DataFrame(response.json())
print(df[["email", "status", "technical_status", "score", "reason"]])

The Actor also accepts comma- or newline-separated addresses inside one list entry. Each input address produces one dataset row, which makes the output straightforward to join back to a CRM export.

How to route the results

Use status for the business decision and technical_status for the underlying verification state:

send = df[df["status"] == "good"]
review = df[df["status"] == "risky"]
suppress = df[df["status"] == "bad"]

# A stricter B2B policy can exclude free and role-based mailboxes.
b2b_send = send[(~send["free"]) & (~send["role"])]

print(f"send: {len(b2b_send)}")
print(f"review: {len(review)}")
print(f"suppress: {len(suppress)}")

The technical states are intentionally explicit:

  • valid: the recipient was accepted and the catch-all control did not undermine the signal;
  • invalid: bad syntax/infrastructure or a definitive SMTP rejection;
  • catch_all: requested and randomized recipients were both accepted;
  • disposable: known temporary-email provider;
  • unknown: the provider timed out, deferred, blocked, or otherwise hid mailbox validity;
  • error: the row could not be processed.

What does the SMTP evidence look like?

The compact fields are designed for filters. verification_details is designed for audits:

{
  "email": "test@gmail.com",
  "domain": "gmail.com",
  "status": "bad",
  "technical_status": "invalid",
  "score": 20,
  "reason": "smtp_rejected",
  "free": true,
  "role": false,
  "disposable": false,
  "catch_all": false,
  "has_tag": false,
  "error": "none",
  "verification_details": {
    "checks": {
      "syntax_valid": true,
      "mx_records_valid": true,
      "smtp_reachable": true,
      "mailbox_exists": false,
      "catch_all_domain": false
    },
    "smtp": {
      "status": "rejected",
      "mx_host": "gmail-smtp-in.l.google.com",
      "response_code": 550,
      "response_message": "550 5.1.1 The email account does not exist",
      "latency_ms": 701
    }
  }
}

That response code is stronger evidence than “the domain has MX.” It is still not permanent truth: a mailbox can be disabled after verification, and a future message can be rejected for sender or content reasons.

Why do some real addresses return unknown?

Large providers defend their recipient directory. They may delay the SMTP greeting, greylist unfamiliar IPs, accept every address, or reject verification-style traffic without saying whether the mailbox exists. In those cases the honest answer is unknown.

The Actor tries one primary MX host with a bounded timeout. Retrying every backup server can turn one guarded domain into a slow, expensive run without improving the answer. Domain-level unknown and catch-all behavior is cached within the run, so a 10,000-row list does not repeatedly hammer the same provider.

Optional sender-domain health

Enable checkDomainHealth when you need SPF, DKIM, and DMARC signals in the same row:

health = pd.json_normalize(df.to_dict("records"))
weak = health[
    (health["domainHealth.spf"] == False) | (health["domainHealth.dmarc"] == False)
]

DKIM is selector-specific. A false result means the Actor did not find a key at the common selectors it checked; it does not prove the domain has no DKIM configuration.

Common mistakes

Treating accepted as guaranteed delivery. SMTP acceptance is the strongest non-sending mailbox signal, but final delivery also depends on sender reputation, authentication, content, and later mailbox state.

Treating catch-all as valid. A catch-all server accepts a fabricated address too. The domain can receive mail, but the person-level mailbox is unconfirmed; keep it in risky.

Dropping every role or free-provider address. That can make sense for a named-person B2B sequence, but it is wrong for support workflows, consumer products, and newsletters. Use the flags according to the campaign.

Retrying unknown providers indefinitely. Repeated SMTP probes can trigger stricter throttling. The Actor uses conservative timeouts and per-domain serialization for that reason.

Related workflows

Run the Email Verifier & Validator on Apify to get mailbox-level evidence, catch-all detection, and auditable results without another verification subscription.

Frequently asked questions

Does this confirm that an email is deliverable?

It performs a non-sending SMTP recipient check and can confirm when a mail server accepts or rejects a mailbox. It still cannot guarantee future delivery: providers can hide recipient status, catch-all domains accept every address, and delivery also depends on sender reputation and message content. Ambiguous results stay unknown.

Does the Actor send an email during verification?

No. It opens an SMTP envelope conversation, checks the recipient with RCPT TO, and disconnects before DATA. No subject, body, or message is transmitted.

How are catch-all domains handled?

After a recipient is accepted, the Actor probes a randomized address at the same domain. If both are accepted, technical_status is catch_all and the row is risky because SMTP cannot prove that the individual mailbox exists.

What do good, risky, and bad mean?

Good means SMTP accepted the mailbox with no high-risk classification. Risky covers catch-all domains, role addresses, and providers that return no decisive signal. Bad covers invalid syntax or infrastructure, disposable addresses, and definitive SMTP rejection.

Can I verify thousands of emails cheaply?

Yes. The Actor runs at 256 MB, caches DNS and domain-level SMTP behavior, throttles requests per domain, and charges per output row. GOLD pricing starts at $0.60 per 1,000 results.

Related