Most teams buy an email verification API after a campaign goes badly. The bounce rate comes back at 9%, deliverability drops for a month afterwards, and somebody goes looking for a tool.
Then they evaluate three vendors by uploading the same list to each and comparing how many addresses come back "valid." Whoever says valid most often wins.
That comparison is backwards, and understanding why requires knowing what verification actually does underneath. This post covers the mechanics, the verdicts that matter, the one case that causes most of the damage, and how to evaluate a provider properly.
What verification actually checks
Verification is a sequence of increasingly expensive checks, each one able to reject an address before the next runs. Roughly in order:
Syntax. Does the address conform to the format rules? This catches typos and malformed input. It is free, instant, and catches less than people expect, because [email protected] is perfectly valid syntax and completely wrong.
Domain and MX records. Does the domain exist, and does it publish mail exchanger records? A domain with no MX cannot receive mail at all, so every address on it is dead. This is a DNS lookup and it is fast.
Provider classification. Is this a disposable address from a throwaway service? A relay or masking service that forwards elsewhere? A role account rather than a person? Each of these is deliverable but behaves differently, and lumping them into "valid" is how bad lists get built.
Mailbox check. Does this specific mailbox exist on this specific server? This is the expensive one and the only one that answers the question you actually asked.
The last step is why verification is a service rather than a library. Everything above it you could build in an afternoon. The mailbox check is where the difficulty lives.
Why the mailbox check is hard
To check whether a mailbox exists, you open a conversation with the receiving mail server and ask, without sending anything. The server either indicates the recipient is acceptable or rejects it.
In principle that is a clean answer. In practice, three things get in the way.
Many servers refuse to tell you. Rejecting unknown recipients at this stage leaks information about which addresses exist, which is useful to spammers. So a large share of mail servers accept every recipient at this point and decide later, after the message is actually sent. Those servers cannot give you a straight answer, and no verification service can extract one.
Reputation gates the conversation. Mail servers are hostile to unknown parties making a lot of these queries. Rate limits, greylisting, and outright blocks are normal. A provider's ability to get answers depends on infrastructure and standing that took time to build, which is most of what you are paying for.
Answers are not stable. A mailbox that exists today may not exist next month. Verification is a snapshot with a short shelf life, not a permanent property of an address.
The practical consequence: any provider claiming near-perfect accuracy on every address is either misunderstanding the problem or hoping you do. A meaningful share of the internet's mail servers structurally cannot be verified, and the honest thing to do is report that clearly rather than guess.
Catch-all domains are the whole ballgame
That second case has a name, and it is where most verification value is won or lost.
A catch-all domain accepts mail for every address, whether or not the mailbox exists. [email protected] and [email protected] both come back accepted. The server is not lying, it genuinely will accept both. It just discards or bounces the second one later, after delivery, which is exactly when a bounce hurts you.
This matters enormously because catch-all is common at precisely the companies B2B teams target: mid-market and enterprise organizations with mail infrastructure configured years ago by someone who is no longer there.
Here is the failure that costs money. A verification tool that reports catch-all as "valid" hands you a list that looks 95% clean. You load it into a sequence. Bounces arrive over the following weeks, mixed in with normal sending, and by the time the pattern is visible your sending reputation has already absorbed the damage. Reputation damage suppresses delivery of mail that would otherwise have arrived, so the cost is not just the bounced addresses, it is the good ones that stopped landing.
A verification API that does not report catch-all separately is not usable for cold outreach. That is the single most important thing on this page.
What to do with catch-all addresses instead of pretending they are clean:
- Put them in their own segment with their own sending schedule and their own bounce monitoring.
- Send to them at lower volume from a domain you are willing to risk, not your primary.
- Corroborate them another way. An address that also resolves to a real person at that company through reverse lookup is meaningfully more likely to be real than one that does not.
- Accept a higher bounce rate on that segment as the cost of reaching those companies at all, and budget for it explicitly.
The verdicts a good API returns
A boolean is not enough. "Deliverable: true" collapses several genuinely different situations into one answer and forces you to treat them identically.
The Renidly Email API returns deliverable alongside a reason enum, plus a separate catch_all flag and the mx_hosts it found. Eight distinct verdicts, each implying a different action:
reason | What it means | What to do |
|---|---|---|
mailbox_accepts | The mailbox exists and accepts mail | Send |
mailbox_rejected | The mailbox does not exist | Suppress permanently |
catch_all | Domain accepts everything, mailbox unconfirmable | Separate segment, monitor closely |
risky | Deliverable with warning signals | Low-volume segment only |
invalid_syntax | Malformed address | Fix at point of capture, this is a form bug |
disposable | Throwaway provider | Suppress, and block at signup |
relay | Forwarding or masking service | Suppress from cold outreach |
no_mx | Domain has no mail exchanger | Suppress permanently |
Two of these deserve comment because teams routinely mishandle them.
invalid_syntax is a bug report, not a data problem. If malformed addresses are reaching your verification step, your form is accepting them. Fix the validation and the problem stops recurring, rather than paying to detect it forever.
disposable at signup is a product signal. A meaningful share of throwaway addresses in your funnel usually means people are trialling without committing, or that your gate is in the wrong place. That is worth knowing independently of deliverability.
1from renidly import Renidly
2
3renidly = Renidly() # reads RENIDLY_API_KEY from the environment
4
5check = renidly.emails.verify("[email protected]")
6
7print(check.deliverable) # True / False
8print(check.reason) # mailbox_accepts, catch_all, risky, ...
9print(check.catch_all) # True when the domain accepts everything
10print(check.mx_hosts) # the mail servers foundIf your stack is Node, the same call with camelCase field names on the response:
1import { Renidly } from "renidly";
2
3const renidly = new Renidly();
4
5const check = await renidly.emails.verify("[email protected]");
6
7console.log(check.deliverable, check.reason, check.catchAll, check.mxHosts);The branching that matters is on reason and catch_all together, never on deliverable alone:
1def route(check) -> str:
2 if check.reason in ("mailbox_rejected", "no_mx", "invalid_syntax"):
3 return "suppress_permanent"
4 if check.reason in ("disposable", "relay"):
5 return "suppress_outreach"
6 if check.catch_all:
7 return "catch_all_segment" # NOT the main list
8 if check.reason == "risky":
9 return "low_volume_segment"
10 return "send"Verifying a list, not an address
One address at a time is fine for a signup form. For a list, use the batch endpoint: up to 1000 addresses per job, submitted in one request, processed in the background, results keyed back to what you sent.
1job = renidly.emails.verify_batch([r.email for r in rows])
2
3for row in job.stream():
4 persist(row.matched_input, row)1const job = await renidly.emails.verifyBatch(rows.map((r) => r.email));
2
3for await (const row of job.stream()) {
4 await persist(row.matched_input, row);
5}Streaming rather than waiting means a crash at 80% does not discard 80% of the work. matched_input links every result back to the exact address you submitted, so you are never zipping two arrays by position and hoping the order held. For jobs larger than 1000 addresses, and for the resumability pattern that saves you re-paying for work already done, see batch enrichment at scale.
How to evaluate a verification provider
Now the part that matters if you are choosing one.
Do not compare "valid" counts. This is the trap in the opening. A provider can inflate its valid count by classifying catch-all as deliverable, and it will win every naive bake-off while producing the worst real-world outcome. The provider reporting fewer valid addresses is often the more accurate one.
Build a known-answer set. Take 100 addresses where you already know the truth. Your own colleagues' addresses, past deliveries you can confirm, and addresses you know hard-bounced. Run that set through each provider and score against reality rather than against each other. Two providers agreeing on a wrong answer looks identical to two agreeing on a right one.
Include deliberately hard cases. Catch-all domains you know are catch-all. Role accounts. Addresses at very small companies. Non-US domains. If your test set is all addresses at large US tech companies, everyone scores well and you learn nothing.
Test the unverifiable share. Count how many addresses each provider marks catch-all, unknown, or risky. A provider reporting a very low unverifiable share is not better at verification, it is making a call where the honest answer is uncertainty. You want that share reported, not hidden.
Measure the outcome, not the verdict. The only number that finally matters is the bounce rate on mail you actually send after filtering. Run a real campaign segment through each provider's output and compare. This takes longer and it is the only test that cannot be gamed.
The broader framework for this, including building a golden set and calculating real cost per usable record rather than cost per call, is in how to test a B2B data vendor.
Costs, and where the money actually goes
Verification is usually priced per address checked, and every address is billed regardless of the verdict, because determining that a mailbox does not exist takes the same work as determining that it does. That is the correct model and you should be suspicious of anything that only charges for "valid" results, since it creates an incentive to call things valid.
Two habits keep the spend sane.
Do not re-verify what has not changed. Verification results have a shelf life measured in months, not days. Cache the verdict with a timestamp and re-check on a schedule tied to how fast that segment decays rather than before every send. The reasoning behind field-level refresh intervals is in CRM data decay.
Filter before you verify. Addresses you already know are dead, already suppressed, or already opted out should never reach the verification step. This sounds obvious and is skipped constantly, usually by a bulk job that re-processes the entire table each run.
Renidly reports credits consumed on every response, so cost per clean address is something you read off your own logs rather than reconstruct from an invoice at month end. Identical repeat calls are served from cache. Current per-route costs and the free tier are on the pricing page.
Verification is one half of the job
Worth stating plainly, because it is a common gap: verification tells you an address works. It does not tell you whose it is, whether they still work there, or whether they are worth contacting.
An address can verify perfectly and belong to someone who left the company four months ago, because the mailbox is still open and forwarding. Deliverable and useless are compatible states.
If your workflow is outbound, you want three things in sequence: find the address, confirm it is deliverable, and confirm the person is still in the role you think they are. The Email API covers the first two. The third is a movement question, and the pattern for catching role changes before you send rather than after is in job change signals. Teams running this end to end usually build it as one pipeline, and the outbound prospecting page has the shape of it.
For inbound, the sequence inverts. An address arrives from a form, and you want to know who it belongs to before deciding what happens next. That is inbound lead enrichment, and the implementation patterns are in reverse email lookup in your signup flow.
The argument against verifying everything
Two situations where verification is not the answer.
If your bounce rate is already under 2%, you likely have a targeting problem rather than a hygiene problem. Verification will not improve reply rates on a list of people who were never going to care. It is a floor, not a lever.
If you are sending to people who opted in recently, verification adds little. They gave you the address, it worked when they gave it to you, and the decay has not had time to accumulate. Verification earns its cost on cold lists, purchased lists, and anything more than a few months old.
And there is a real risk of over-suppression. Aggressively removing everything marked risky or catch-all will delete a meaningful share of legitimate contacts at exactly the mid-market and enterprise companies you probably most want to reach. Segment them rather than deleting them.
The one-line version
Never branch on a boolean. Treat catch-all as its own segment rather than as valid, because that single decision drives most of the bounce damage teams attribute to bad data. Evaluate providers against a known-answer set and a real bounce rate, not against each other's valid counts, and be suspicious of any provider reporting an unusually low share of unverifiable addresses.
If you want to test that on your own list, there are 100 free credits with no card required, which covers a real sample rather than a demo file. The Quickstart takes about five minutes, and the endpoint playground will run a single address without writing any code at all.


