Email Verification APIs: How They Work and When to Use One

An envelope travels along a pipeline through shield, magnifier and check badge checkpoints to a server.

An email verification API takes an address and returns whether it is likely to accept mail. It runs syntax, domain (MX), and mailbox-level checks, then adds risk flags such as disposable, role-based, and catch-all. That cuts hard bounces and fake signups. It cannot prove that a human reads the inbox, and for a large share of addresses it can only answer “unknown”.

Vendors use “verification” and “validation” for the same product, so this post uses both.

What each check does, and which ones you can run yourself

Most of a verification API’s output is built from a handful of checks. Some are cheap enough to run in your own code. The mailbox check is the one that justifies paying for a service.

CheckWhat it testsCan you do it yourself?Reliability
SyntaxAddress fits the grammar and length limitsYes, in a few lines. See the regex syntax checkHigh for rejecting garbage. A strict regex will also reject some legal addresses
Domain exists and accepts mailDNS returns an MX record, or an A/AAAA record when there is no MXYes, one DNS queryHigh. Temporary DNS failures are not proof the domain is bad
Disposable domainDomain is on a list of throwaway providersOnly with a list you maintainMedium. Lists go stale as new throwaway domains appear
Role accountLocal part is admin, info, support and similarYes, a short listHigh for detection, but a role address can still be deliverable
Typo suggestionDomain is close to a common one (gmial.com)Yes, with an edit-distance check against popular domainsGood as a suggestion, wrong as a hard rule
SMTP mailbox probeServer’s reply to RCPT TO for that mailboxTechnically yes, practically no (see below)Low to medium, and strongly dependent on the receiving server
Catch-all detectionServer accepts a deliberately random mailboxSame as the probeDetects the condition, cannot say whether your address is real

The limits that decide your syntax check

RFC 5321 section 4.5.3.1 sets the sizes: a local part of at most 64 octets (4.5.3.1.1), a domain of at most 255 octets (4.5.3.1.2), and a path of at most 256 octets “including the punctuation and element separators” (4.5.3.1.3). The path is written inside angle brackets, so the address itself fits in 254 characters. RFC 5321 never states 254 as a number; it is derived from the 256 limit, and it is the figure most validators enforce. The grammar lives in RFC 5322, but the length limits come from RFC 5321.

The MX lookup has a fallback

RFC 5321 section 5.1 says delivery starts with an MX lookup. “If an empty list of MXs is returned, the address is treated as if it was associated with an implicit MX RR, with a preference of 0, pointing to that host.” So a domain with no MX record but a working A record can still receive mail, and a check that demands an MX will wrongly reject it. The opposite case exists too: RFC 7505 defines a “null MX” (a single MX record with the target .) for domains that accept no mail at all. Running the lookup from the example below against example.com returns exactly that.

Why the SMTP probe is the hard part

The probe connects to the domain’s mail server, issues MAIL FROM and RCPT TO for the address, reads the reply, and quits before sending DATA, so no message is delivered. RFC 5321 section 4.1.1.3 describes the intended outcome: if the recipient “is known not to be a deliverable address, the SMTP server returns a 550 reply”. Whether you get that honest answer depends on the server.

  • Catch-all domains accept everything. Mailgun’s documentation says the validity of the address “cannot be determined as the provider accepts any and all email regardless of whether or not the recipient’s mailbox exists.” See catch-all domains for how to handle them.
  • Accept now, bounce later. Mailgun’s accept_all reason adds: “Invalid addresses may generate delayed bounce notifications after delivery is attempted.” The probe sees a success reply and the bounce arrives afterward.
  • Greylisting returns temporary errors. RFC 6647 defines greylisting’s narrow sense as “generation of an SMTP temporary failure reply code” for senders the server does not know. RFC 5321 section 4.2.1 puts those in the 4yz class: the error is temporary and the action “may be requested again”. A probe that gets a 4yz has learned nothing about the mailbox.
  • Servers are allowed to say nothing useful. RFC 5321 section 6.2 notes that “accepting a message may trigger additional undesirable traffic by providing verification of the address”, so operators have a reason to withhold it. Section 7.3 says sites may disable VRFY for security reasons.
  • Probing from your own IP is risky. Practitioners commonly report that opening many connections that never send mail can get an IP throttled or blocklisted, which hurts your sender reputation. This is operator experience, not something a standard specifies.

That is why a good API returns “unknown” and “risky” instead of forcing a yes or no. A catch-all result means unknowable, not valid. If a vendor marks every catch-all address “valid”, it is guessing.

Result categories and what to do with each

Vendors name these differently. From their own documentation:

  • Mailgun Validate result: deliverable, undeliverable, do_not_send, catch_all, unknown, with a separate risk of high, medium, low, or unknown.
  • ZeroBounce status: valid, invalid, catch-all, unknown, spamtrap, abuse, do_not_mail, with sub-statuses such as disposable, role_based, and greylisted.
  • Kickbox result: deliverable, undeliverable, risky, unknown, with role, disposable, and accept_all as booleans.
  • SendGrid verdict: Valid, Risky, Invalid, plus a numeric score.

Map them to your own small set before they reach application code. A starting policy:

Typical statusAt signupWhen importing a list
Valid / deliverableAcceptKeep
Invalid / undeliverableReject with a clear messageRemove. These are the addresses that produce bounce rate spikes
Catch-all / accept-allAccept, then confirm by emailKeep, but send low-risk mail first and watch bounces
UnknownAccept. Do not block on itKeep in a separate segment and retry later
Risky / do not sendAccept with a confirmation stepSuppress unless the address engaged recently
DisposableYour call: reject if you need long-term contact, accept if you do notRemove from marketing lists
Role (info@, support@)Accept for B2B accountsKeep for transactional mail, exclude from marketing
Typo suggestedShow “Did you mean…?” and let the user overrideFlag for review

When to call it: signup versus batch

At signup, fail open. Treat the API as an optional improvement to your form, not a gate your form depends on. A short timeout, an outage, a rate limit, and an “unknown” result should all end with the user getting in. Block only on results that are certain: failed syntax, a domain that cannot receive mail, or a mailbox the server explicitly rejected. For likely typos, suggest the fix instead of showing an error, since a hard error on a legitimate address costs you a customer.

In batch, before mailing a list. Verify an old list before importing it or re-mailing it, as part of cleaning an existing list. Here latency does not matter, and you can retry unknowns on a later pass.

Verification does not replace consent controls. It tells you whether a mailbox probably exists, not whether its owner asked for your mail. Double opt-in proves both, and it is the strongest defense against spam traps. Verification will not reliably find pristine traps, which are addresses created only to catch senders who collected addresses without consent. Emailable, a verification vendor, says so directly: “Email Verifiers can reduce risk and improve list quality, but cannot reliably identify a provider’s spam trap.” Some vendors do report known or likely traps (ZeroBounce has a spamtrap status and a possible_trap sub-status), but a pristine trap has no history for a vendor to flag.

A signup check that fails open

This Node.js 18+ function runs the local checks first, then calls a verification endpoint with a 2.5 second timeout. The endpoint, the Authorization header, and the response fields (status, disposable, suggestion) are placeholders in the same provider-agnostic style as the Python email example. Replace them with your vendor’s API reference.

import { resolveMx, resolve4, resolve6 } from "node:dns/promises";

// Placeholder endpoint and payload, not any vendor's real API.
const ENDPOINT = process.env.VERIFY_URL ?? "https://api.your-provider.com/v1/verify";
const TIMEOUT_MS = 2500;

// RFC 5321 4.5.3.1: local-part 64 octets, path 256 octets (254 once "<" and ">" are excluded).
function syntaxOk(email) {
  const at = email.lastIndexOf("@");
  if (at < 1 || email.length > 254 || at > 64) return false;
  return /^[^\s@]+@[^\s@.]+(\.[^\s@.]+)+$/.test(email);
}

// "yes" | "no" | "unknown". Follows RFC 5321 5.1: MX first, A/AAAA only if there is no MX.
async function domainAcceptsMail(domain) {
  try {
    const mx = await resolveMx(domain);
    return mx.some((r) => r.exchange !== "") ? "yes" : "no"; // a lone "." is a null MX (RFC 7505)
  } catch (err) {
    if (err.code === "ENOTFOUND") return "no";
    if (err.code !== "ENODATA") return "unknown"; // SERVFAIL, timeout: do not guess
  }
  try {
    const [a, aaaa] = await Promise.allSettled([resolve4(domain), resolve6(domain)]);
    return a.status === "fulfilled" || aaaa.status === "fulfilled" ? "yes" : "no";
  } catch {
    return "unknown";
  }
}

export async function checkSignupEmail(email) {
  if (!syntaxOk(email)) return { action: "reject", reason: "syntax" };

  const domain = email.slice(email.lastIndexOf("@") + 1);
  if ((await domainAcceptsMail(domain)) === "no") return { action: "reject", reason: "domain" };

  try {
    const res = await fetch(ENDPOINT, {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.VERIFY_API_KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ email }),
      signal: AbortSignal.timeout(TIMEOUT_MS),
    });
    if (!res.ok) return { action: "accept", reason: `vendor_http_${res.status}` }; // fail open
    const v = await res.json();

    if (v.suggestion) return { action: "warn", reason: "typo", suggestion: v.suggestion };
    if (v.status === "invalid") return { action: "reject", reason: "mailbox" };
    if (v.status === "valid" && !v.disposable) return { action: "accept", reason: "valid" };
    // catch_all, risky, unknown, disposable: let them in, confirm by email instead
    return { action: "warn", reason: v.disposable ? "disposable" : v.status };
  } catch {
    return { action: "accept", reason: "vendor_unavailable" }; // timeout or network error: fail open
  }
}

The function was tested on Node.js 24 against a local mock server and real DNS. Results: a valid reply returned accept; invalid returned reject; catch-all, disposable, and a typo suggestion returned warn; a server that never answered returned accept after 2.5 seconds; an HTTP 500 returned accept. A bare string with no @, a 65-character local part, and example.com (null MX) were rejected locally, and www.example.com (no MX, A record only) passed the domain check. gmail.com returned five MX records.

Call checkSignupEmail from your signup handler, show the suggestion on warn, and send a confirmation email either way. That confirmation step is what turns “probably real” into “confirmed”. If you send that email through a transactional email API, a rejected recipient shows up as a 550 error, which is the case the checks above exist to catch earlier.

Comparing providers

Prices below were copied from each vendor’s public pages on October 8, 2026. They change, and several vendors run promotions.

ProviderReal-time APIBulk / list uploadPublished pay-as-you-go priceFree credits
ZeroBounceYesYesFrom $39 for 2,000 credits (minimum purchase)100 per month with a business-domain signup
KickboxYesYes$10 for 1,000; $70 for 10,000; $500 for 100,000100
EmailableYesYes$38 for 5,000 credits ($0.0076 per credit, 5,000 minimum)250 on new accounts
Mailgun (Validate)YesYesAdd-on to a plan: $1.20 per 100 for the first 5,000 extra validations (Basic and Foundation)Not published. Scale plan includes 5,000
Twilio SendGrid (Email Address Validation API)YesYes (up to one million addresses)Not published as a standalone price2,500 included with Pro (from $89.95/month); 5,000 with Premier

Kickbox showed a 30% promotion on its pricing page; the figures above are its list prices. Kickbox refunds the credit for an unknown result and Emailable states that unknown results are free, which matters if you expect many catch-all domains. NeverBounce is not listed because its pricing page was not retrievable when this was checked.

Frequently Asked Questions

What is an email verification API?

An email verification API is an HTTP endpoint that takes an email address and returns a verdict on whether it can receive mail. It combines syntax, DNS, and SMTP mailbox checks with flags for disposable, role-based, and catch-all addresses.

Is email validation the same as email verification?

In practice, yes. Vendors use “validation” and “verification” for the same product category. Some writers reserve “validation” for format and domain checks and “verification” for the mailbox check, but API names and docs do not follow that split consistently.

How accurate are email verification APIs?

They are reliable at rejecting malformed addresses and non-existent domains. Mailbox-level accuracy varies, because catch-all domains, greylisting, and servers that accept mail and bounce later cannot be resolved by a probe. Vendors return “unknown” or “risky” for those, and you should treat those results as uncertain rather than valid.

Can you verify an email without sending one?

Mostly. Syntax and DNS checks send nothing, and the SMTP probe stops before the DATA command, so no message is delivered. The only way to confirm that a person controls and reads the address is to send a message, which is what double opt-in does.

Does verifying emails hurt deliverability?

Verifying through a vendor does not touch your sending reputation, since the vendor’s servers make the connections. Running your own SMTP probes from your sending IP can, because repeated connections that never deliver mail may get that IP throttled. Verification itself helps by keeping bounces low.

Should I verify emails at signup?

Yes, if invalid addresses are costing you bounces or support time, as long as the check fails open. Use a short timeout, never block on “unknown”, suggest typo fixes instead of rejecting, and follow up with a confirmation email.

How much does email verification cost?

Published list prices run from about $7.60 per 1,000 addresses (Emailable at 5,000 credits) to $10 per 1,000 (Kickbox at 1,000), and ZeroBounce’s entry package works out to $19.50 per 1,000. Some providers bundle a quota into a sending plan instead. See the provider table above for the date these were checked.