Email Verification API: How It Works and Why Bounces Cost You

An email verification API checks an address before you send to it: is it well formed, does the domain accept mail, is the mailbox real, and is the domain a catch-all that accepts everything. Each check is explained below in the order it runs, with the status and flags it produces, why bounces damage sending reputation, and the code to run a list through the Locomint verify endpoint.

The seven checks an email verification API runs, in order

Verification is a series of checks run in order, each more expensive than the last. An email verification API stops at the first failure, which is why a badly typed address comes back in milliseconds while a real mailbox on a slow mail server can take seconds.

  1. Syntax. The string needs one @, a local part, a domain with at least one dot, and no spaces or stray characters. The formal grammar is RFC 5322; almost every checker applies a stricter practical subset. A failure here is invalid, with syntax_valid: false.
  2. Domain and MX records. The domain must exist and publish mail exchanger records in DNS. No MX record and no fallback address record means nothing can deliver, whatever comes before the @: undeliverable, with mx_found: false.
  3. Disposable providers. A throwaway domain that hands out ten-minute inboxes may deliver today and be dead tomorrow, so it gets its own status, disposable.
  4. Free providers. A consumer mailbox on a webmail service delivers fine. free_provider: true is there so a business list can prefer company domains.
  5. Role accounts. info@, sales@ and support@ are the addresses most businesses want written to. role_account: true marks them, and they behave differently from personal mailboxes on catch-all domains, below.
  6. Catch-all detection. The verifier offers the mail server a deliberately nonsensical address on the same domain, once per domain. If the server accepts it, it accepts everything, and no mailbox on that domain can be individually confirmed.
  7. SMTP mailbox check. The server is asked whether it accepts this specific recipient. It is the only step that answers the question you asked, and the one most tools skip.

Why bounces cost more than the bad address

A bounced message is not just a wasted send. Mailbox providers watch the share of your traffic that bounces, and they treat a high rate as the signature of a purchased or scraped list. The consequences arrive in stages: first your messages land in spam for the addresses that were fine, then your sending domain or IP is throttled, and eventually your email service provider suspends the account to protect its other customers.

The damage is shared. Bad addresses in one campaign lower delivery for every campaign that follows from the same domain, including transactional mail such as invoices and password resets. Rebuilding reputation means sending less, to your cleanest addresses, until the numbers recover. Verifying a list beforehand costs one credit per address.

Email service providers publish their own bounce thresholds and they differ, so check the policy page of whichever tool you send from rather than a number from a blog post. The direction is the same everywhere: keep hard bounces as close to zero as you can.

How to verify an email address without sending a message

The mailbox check works by starting a normal mail delivery and stopping before any content is sent. The protocol that mail servers speak, defined in RFC 5321, is a conversation of short commands, and the server answers each with a status code before the next one is allowed.

A verifier connects to the domain's mail exchanger on port 25, introduces itself with EHLO, names a sender with MAIL FROM, and then names the address under test with RCPT TO. The server's reply to that last command is the answer. A code in the 250 range means the recipient is accepted; a 550 means the mailbox does not exist; a 450-series code means try later. The verifier then sends QUIT and closes the connection. No DATA command is ever issued, so no message exists, nothing lands in the inbox, and the mailbox owner never knows a check happened.

Not every server cooperates. Some large providers answer every RCPT TO with acceptance and sort out the unknown addresses later. Others rate-limit or refuse connections from hosts they have not seen before. Locomint opens at most a couple of connections per mail server, does not retry a refusal, and returns unknown with the smtp_code it saw when the server would not say, rather than guessing. When the server could not be asked at all, the row comes back valid with smtp_checked: false, meaning syntax and MX passed and nothing more.

Catch-all domains: why the result is risky, not deliverable

A catch-all domain is configured to accept mail for any local part. Send to anything-at-all@example.com and the server says yes, then either forwards it to a shared inbox or silently discards it. Companies set this up so that misspelled addresses still arrive, and many hosting control panels turn it on by default.

The SMTP check then becomes uninformative: the server accepts the real address and a fabricated one with the same 250 code, so a positive answer proves nothing about the mailbox. The verifier detects this once per domain by testing a random address, then marks every real address on that domain risky with catch_all: true, rather than deliverable.

Risky is not bad. A role mailbox the company printed on its own website is very likely to exist even on a catch-all domain; a guessed personal address on the same domain is much less likely to. Treat risky rows as a separate segment: send to them in smaller batches, and let the bounce data decide whether to keep them.

Reading the response: statuses, reasons and scores

Every address you submit comes back as one row with a status, a short reason, a score from 0 to 100, and a flag for each check that ran.

StatusMeaningWhat to do
deliverableSyntax, domain and mailbox all confirmedSend
validSyntax and MX passed; the mailbox was not asked (smtp_check off, or the server could not be reached)Treat as domain-level only; re-run with the SMTP check when you can
riskyDomain is catch-all; mailbox cannot be individually confirmedSend in small batches, monitor bounces
undeliverableDomain has no mail server, or the server rejected the mailboxDrop
invalidThe string is not a well-formed addressDrop, or fix the typo if it is obvious
disposableThrowaway providerDrop from business lists
unknownThe mail server would not answer the mailbox questionKeep aside; re-check later or treat like risky

The flags let you apply your own policy. syntax_valid, mx_found and mx_host describe the domain. disposable, free_provider and role_account classify the address. catch_all, smtp_checked and smtp_code say how far the mailbox check got and what the server answered. To clean a form submission quickly, pass "smtp_check": false; the call returns without contacting any mail server, and passing rows come back valid rather than deliverable.

Calling the email verification API

The verify endpoint accepts up to 100 addresses per request and charges one credit per address. Authentication is a single X-API-Key header. From the shell:

curl -X POST "https://api.locomint.io/v1/emails/verify" \
  -H "X-API-Key: $LOCOMINT_KEY" -H "Content-Type: application/json" \
  -d '{"emails": ["info@example.com", "sales@example.com", "not-an-address"]}'

In Python with httpx, the same call plus a sort into three buckets you can feed to a mailing tool:

import httpx

API = "https://api.locomint.io/v1"
headers = {"X-API-Key": "lm_free_your_key_here"}

addresses = ["info@example.com", "sales@example.com", "hello@example.org"]
send, review, drop = [], [], []

with httpx.Client(base_url=API, headers=headers, timeout=120) as client:
    for start in range(0, len(addresses), 100):
        batch = addresses[start:start + 100]
        rows = client.post("/emails/verify", json={"emails": batch}).json()["data"]
        for row in rows:
            if row["status"] == "deliverable":
                send.append(row["email"])
            elif row["status"] in ("risky", "unknown"):
                review.append((row["email"], row["reason"], row["score"]))
            else:
                drop.append((row["email"], row["status"]))

print(len(send), "ready to send")
print(len(review), "to review")
print(len(drop), "dropped")

The loop slices the list into groups of 100 because that is the per-call maximum. The client timeout is 120 seconds because a batch that hits several slow mail servers takes time, and the API waits for real answers rather than returning early with guesses. It does not wait forever: a call that has not finished within 90 seconds returns 504 timeout, nothing is charged, and the fix is to send fewer addresses per call.

Deduplicate and lower-case the addresses before you submit them. The same mailbox written twice with different capitalisation costs two credits and produces two rows, and a list exported from a CRM almost always contains a few. On the free plan's 200 credits a month, that is the difference between verifying a full list and running out halfway.

Where verification fits in an outreach pipeline

Verification is the last step before sending, not the first step of research. If the addresses come from the Locomint place details endpoint, they arrived already filtered to role mailboxes on the company domain, which removes most of the invalid and personal addresses before verification even starts. The article on finding business email addresses covers that collection step.

A sensible order for a list of local businesses is: search for the businesses, fetch details for the ones with a website, collect enrichment.emails, deduplicate, verify, and only then load the deliverable set into the sending tool. Keep the verification rows alongside the addresses. When a message bounces despite a deliverable verdict, the smtp_code and mx_host tell you whether the mail server changed or the mailbox was closed in between.

Re-run verification on any list older than a couple of months. Addresses decay quietly: a business moves to a new provider, a domain lapses, an inbox is retired. One credit per address is cheap next to finding out by bouncing.

Where to start this week

Take the most recent list you sent to, or the one you are about to send to, and verify the first 100 addresses. Count the statuses. If more than a handful come back undeliverable or invalid, you have found a bounce problem you may not have noticed yet; verify the rest before the next send.

The free plan's 200 credits cover that first test without a card. If your lists are larger, the Starter, Growth and Scale plans raise the monthly credits and the same endpoint keeps working unchanged. Create a key on the signup page; it is active immediately.

Frequently asked questions

Does email verification send a message to the address?

No. The check opens a connection to the domain's mail server, announces itself, names a sender and the recipient, reads the server's answer, and closes the connection before any message content is transmitted. The mailbox owner sees nothing. Some mail servers refuse to answer that question at all, and those addresses come back as unknown rather than being guessed.

What should I do with addresses marked risky?

Risky means the domain accepts mail for any address, so the mailbox itself could not be confirmed. Send to those addresses in small batches, separate from your confirmed list, and watch the bounce rate. Role mailboxes such as info@ on a catch-all domain are a better bet than guessed personal addresses on the same domain, because the business published them on purpose.

How long does a verification result stay valid?

A result describes the mailbox at the moment of the check. Businesses change providers, staff leave and domains lapse, so a deliverable verdict from six months ago is a hint, not a guarantee. Re-verify a list before each campaign, or at least any address that has not been used successfully in the last one to three months.