Email Verification APIs Compared: Auth, Timeouts, Rate Limits
Email verification APIs all run the same checks, so choose on integration surface: where the key can live, the timeout you can set, and the rate limit you will hit. Those differ sharply. Timeouts are seconds for some vendors and milliseconds for others, limits range from 5 concurrent calls to 80,000 requests per 10 seconds, and statuses never match.
Everything below comes from each vendor's own API documentation, read on 1 October 2026 and linked in each section. We did not hold accounts with all nine, so there are no latency measurements here, and no claims about which vendor is more accurate.
The comparison table
The single-address endpoint is the one you call from a signup form or a webhook, so that is what is compared. Bulk and batch endpoints have separate limits.
| Vendor | Endpoint | Auth | Timeout parameter | Published rate limit |
|---|---|---|---|---|
| ZeroBounce | GET api.zerobounce.net/v2/validate |
api_key query param |
timeout, seconds, 3–60, default 30 |
80,000 requests per 10 s; excess blocked for 1 minute |
| NeverBounce | GET api.neverbounce.com/v4.2/single/check |
key query param |
timeout, seconds, no default stated |
Not stated in the public reference |
| Kickbox | GET api.kickbox.com/v2/verify |
apikey param or Authorization: Bearer |
timeout, milliseconds, default 6,000, max 30,000 |
25 parallel requests per IP; 8,000 per clock minute |
| Emailable | GET/POST api.emailable.com/v1/verify |
api_key param or Authorization: Bearer |
timeout, seconds, 2–10, default 5 |
25 per second |
| Hunter | GET api.hunter.io/v2/email-verifier |
api_key param, X-API-KEY, or Bearer |
None; returns 202 after 20 s |
10 per second and 300 per minute |
| Clearout | POST api.clearout.io/v2/email_verify/instant |
Authorization: Bearer |
timeout, milliseconds, 1,000–180,000, default 130,000 |
Per plan, reported in x-ratelimit-* headers over a 60 s window |
| Bouncer | GET api.usebouncer.com/v1.1/email/verify |
x-api-key header |
timeout, seconds, default 10, max 30 |
1,000 per minute by default |
| DeBounce | GET api.debounce.io/v1/ |
api query param only |
None documented | 5 concurrent calls (2 with enrichment) |
| Mailgun | GET/POST api.mailgun.net/v4/address/validate |
HTTP Basic, user api, password = key |
None; provider_lookup toggle |
"A set number of active requests at a time" (number not published) |
Sources: ZeroBounce, NeverBounce, Kickbox and Kickbox limits, Emailable and Emailable limits, Hunter, Clearout, Bouncer, DeBounce, Mailgun.
Two of these are not standalone verification companies. Hunter is primarily an email finder and Mailgun is primarily a sending platform; both are included because developers frequently already hold keys for them.
Five integration traps the docs reveal
Reading nine sets of docs side by side surfaces problems you will not notice reading one.
1. Timeout units are not consistent
Kickbox and Clearout take milliseconds. ZeroBounce, NeverBounce, Emailable and Bouncer take seconds. Send timeout=5 to Kickbox and you have asked for five milliseconds; send timeout=5000 to Emailable and you are outside its documented 2–10 range.
Write the unit into the variable name (timeoutMs, timeoutSeconds) and convert at the edge, in the vendor adapter.
2. The vendor's timeout is not your timeout
NeverBounce says it plainly: its timeout is how long it will try before returning unknown, and "the total request time can exceed this timeout, as network latency is not taken into consideration." The same logic applies everywhere. Set the vendor's timeout a little below your own HTTP client's abort, so the vendor normally answers unknown before your client gives up.
The defaults matter too. Clearout's default of 130,000 ms is over two minutes, which is reasonable for a batch job and far too long for a signup form.
3. "Slow" can come back as a success code
Two vendors hand back slow results in an unusual way:
- Emailable returns HTTP 249 with "Your request is taking longer than normal. Please send your request again."
- Hunter returns 202 Accepted when a check runs past 20 seconds, and you poll the same endpoint. Hunter says these requests "are counted only once."
Code that treats every 2xx as a finished result will try to parse a status that is not there. Check the status code, not just res.ok.
4. Booleans are not always booleans
DeBounce's OpenAPI schema types code as an integer and role as a boolean, but its own example responses return strings: "code": "5", "role": "false", "success": "1". Bouncer's flags are "yes", "no" or "unknown". In Python, bool("false") is True, so a naive check marks every address as a role address.
A small coercion helper avoids it. Tested with Python 3.14:
def as_bool(value):
"""Accept True/False, "true"/"false", "yes"/"no", "1"/"0" and 1/0 alike."""
if isinstance(value, bool):
return value
text = str(value).strip().lower()
if text in {"true", "yes", "1"}:
return True
if text in {"false", "no", "0"}:
return False
return None # "unknown", "", None: genuinely undetermined
debounce = {"debounce": {"code": "5", "role": "false", "send_transactional": "1"}, "success": "1"}
bouncer = {"domain": {"acceptAll": "unknown", "disposable": "no"}}
print(as_bool(debounce["success"]), as_bool(debounce["debounce"]["role"])) # True False
print(as_bool(bouncer["domain"]["disposable"]), as_bool(bouncer["domain"]["acceptAll"])) # False None
Note the third state. Bouncer's "unknown" should stay unknown, not collapse to False.
5. Some defaults switch checks off
Emailable's accept_all parameter defaults to false, and its docs say the accept-all check "heavily impacts API's response time." Mailgun's provider_lookup defaults to true; set it to false and Mailgun may answer unknown with reason no_data rather than contacting the mailbox provider. Know which checks your call is actually running before you compare results between vendors.
Status values, side by side
Every vendor has a "confirmed good", a "confirmed bad" and a grey middle. They name them differently, and some add categories the others fold into reasons.
| Vendor | Status field | Values |
|---|---|---|
| ZeroBounce | status |
valid, invalid, catch-all, unknown, spamtrap, abuse, do_not_mail (plus 26 sub_status values) |
| NeverBounce | result |
valid, invalid, disposable, catchall, unknown |
| Kickbox | result |
deliverable, undeliverable, risky, unknown |
| Emailable | state |
deliverable, undeliverable, risky, unknown |
| Hunter | data.status |
valid, invalid, accept_all, webmail, disposable, unknown |
| Clearout | data.status |
Documented as Valid, Invalid, Catch All, Unknown (plus safe_to_send: Yes, Risky, No, Unknown) |
| Bouncer | status |
deliverable, risky, undeliverable, unknown |
| DeBounce | debounce.result |
Safe to Send, Risky, Invalid, Unknown |
| Mailgun | result |
deliverable, undeliverable, do_not_send, catch_all, unknown (plus risk) |
A few details worth knowing:
- Hunter's
webmailis returned for addresses at providers such as Gmail or Outlook. It is not a verdict on the mailbox, so treat it as unconfirmed unless another field says otherwise. - NeverBounce's
disposableis a top-level result. Elsewhere disposable is a boolean flag alongside the status, so a disposable address can still come backdeliverablefrom Kickbox or Bouncer. - Catch-all has at least four spellings across these APIs:
catch-all(ZeroBounce),catchall(NeverBounce),accept_all(Hunter) andcatch_all(Mailgun). Kickbox and Bouncer do not have a catch-all status at all; it arrives asriskywith reasonlow_deliverabilityand anaccept_allflag. We explain why these cannot be confirmed in what is a catch-all domain.
Our longer guide to verification statuses covers what each grey-area result means for sending.
Normalise before your app sees it
Your application should not branch on nine vocabularies. Pick three outcomes and translate at the boundary. Anything that is not explicitly confirmed good or bad becomes risky, including statuses a vendor adds next year.
Tested with Node.js 22:
// normalise.mjs
const MAP = {
zerobounce: { path: "status", valid: ["valid"], invalid: ["invalid", "spamtrap", "abuse", "do_not_mail"] },
neverbounce: { path: "result", valid: ["valid"], invalid: ["invalid", "disposable"] },
kickbox: { path: "result", valid: ["deliverable"], invalid: ["undeliverable"] },
emailable: { path: "state", valid: ["deliverable"], invalid: ["undeliverable"] },
bouncer: { path: "status", valid: ["deliverable"], invalid: ["undeliverable"] },
hunter: { path: "data.status", valid: ["valid"], invalid: ["invalid", "disposable"] },
debounce: { path: "debounce.result", valid: ["safe to send"], invalid: ["invalid"] },
mailgun: { path: "result", valid: ["deliverable"], invalid: ["undeliverable", "do_not_send"] },
};
const pick = (obj, path) => path.split(".").reduce((o, k) => o?.[k], obj);
export function normalise(vendor, body) {
const rule = MAP[vendor];
if (!rule) throw new Error(`unknown vendor: ${vendor}`);
const raw = String(pick(body, rule.path) ?? "").toLowerCase();
if (rule.valid.includes(raw)) return { outcome: "valid", raw };
if (rule.invalid.includes(raw)) return { outcome: "invalid", raw };
return { outcome: "risky", raw }; // catch-all, unknown, risky, webmail, anything new
}
normalise("zerobounce", { status: "catch-all" }); // { outcome: 'risky', raw: 'catch-all' }
normalise("hunter", { data: { status: "webmail" } }); // { outcome: 'risky', raw: 'webmail' }
normalise("debounce", { debounce: { result: "Safe to Send" } }); // { outcome: 'valid', raw: 'safe to send' }
normalise("bouncer", { status: "brand_new_status" }); // { outcome: 'risky', raw: 'brand_new_status' }
Keep raw and store it. When you later wonder why a batch bounced, the vendor's own word is the evidence. Clearout is left out of the map because we did not confirm the exact casing its JSON uses for data.status; check one live response before adding it.
Where you put disposable and spam-trap results is a product decision. The map above treats ZeroBounce's spamtrap and abuse as invalid, which suits a marketing list; a transactional signup might prefer to let abuse through.
A signup call that fails open
At signup, the API is a helper, not a gate. If it is slow, rate-limited or down, let the person in and mark the address unconfirmed. This uses Kickbox's documented parameters and was tested against a local mock that returned a normal result, a typo, a 6-second delay and a 429.
// signup-check.mjs
import { normalise } from "./normalise.mjs";
const BASE = process.env.KICKBOX_BASE ?? "https://api.kickbox.com";
const KEY = process.env.KICKBOX_API_KEY;
// Ask the vendor to give up at 3s; abort our own request at 4s
// in case the network adds time on top.
export async function checkAtSignup(email) {
const url = new URL("/v2/verify", BASE);
url.searchParams.set("email", email);
url.searchParams.set("timeout", "3000"); // Kickbox: milliseconds
try {
const res = await fetch(url, {
headers: { Authorization: `Bearer ${KEY}` },
signal: AbortSignal.timeout(4000),
});
if (!res.ok) return { outcome: "risky", raw: `http_${res.status}`, failedOpen: true };
const body = await res.json();
return { ...normalise("kickbox", body), suggestion: body.did_you_mean ?? null };
} catch (err) {
return { outcome: "risky", raw: err.name, failedOpen: true };
}
}
Output against the mock:
bill@example.com {"outcome":"valid","raw":"deliverable","suggestion":null} 48ms
bill.lumbergh@gamil.com {"outcome":"invalid","raw":"undeliverable","suggestion":"bill.lumbergh@gmail.com"} 3ms
slow@example.com {"outcome":"risky","raw":"TimeoutError","failedOpen":true} 4002ms
limit@example.com {"outcome":"risky","raw":"http_429","failedOpen":true} 13ms
Those timings are the mock's, not Kickbox's. The point is the shape: the slow case cost exactly the 4-second budget and no more. For the wider design, including what to show the user, see real-time verification on signup forms.
For Emailable you would also treat 249 as "retry later", and for Hunter you would treat 202 the same way. Neither should reach normalise().
Keys in the browser
A private key in front-end JavaScript can be read and spent by anyone. The vendors handle this differently:
- Clearout states its API supports server-side requests only.
- DeBounce offers
public_keys for client-side widgets, with a CORS domain allowlist and a cap of 20 validations per IP per day. - Emailable has separate public keys, used with a
captcha_responseparameter.
If your vendor has no public key, proxy through your own backend and rate-limit that endpoint, or the form becomes a free verification service for anyone who finds it.
Testing without spending credits
Kickbox documents a sandbox mode: a sandbox key returns mock results, deliverable by default, and you force others by address, such as deliverable@example.com or john+deliverable@example.com. Kickbox is explicit that "Results from Sandbox Mode are not real." Emailable mentions test keys with a simulate parameter on its batch endpoint.
Policies on unknown results help too. ZeroBounce says it "will never consume a credit for any unknown result", and Clearout says the same of its Unknown results. That matters when you retry timeouts: retries of unknowns cost nothing on those two.
Choosing, as a developer
There is no single winner, because the right API depends on where you call it from.
| If you need | Look at | Why, from the docs |
|---|---|---|
| High-volume server-side calls | ZeroBounce, Kickbox | Highest published limits (80,000 per 10 s; 8,000 per minute) |
| Short, strict signup timeouts | Emailable, Kickbox | Small default timeouts (5 s and 6 s) and documented typo suggestions |
| A key safe for the browser | DeBounce, Emailable | Public keys with abuse controls |
| Already sending through the vendor | Mailgun | Same account and auth scheme |
| Finder and verifier in one key | Hunter | Verification sits beside its finder endpoints |
| Long, patient checks | Clearout, Bouncer | Timeouts up to 180 s and 30 s respectively |
Whichever you choose, the defensive layer is the same: convert timeout units in one place, cap the wait yourself, coerce string booleans, normalise to three outcomes, keep the raw status, and fail open at signup. Prices for most of these vendors are on our comparison pages.
Disclosure — our product
SimpleVerifier, which publishes this blog, does not offer a public API-key REST API, so it is not an option for the integrations described here. It is used through the web app and CSV upload, at $29.99/month flat, with unconfirmed addresses returned as risky rather than valid.
Common questions
Which email verification API is fastest?
No vendor publishes an audited latency figure, and we did not measure one. What the docs do tell you is how long each API is allowed to wait: Emailable defaults to 5 seconds, Kickbox to 6, Bouncer to 10 and ZeroBounce to 30. Set the timeout parameter yourself rather than relying on any default.
Why does the same address get different statuses from different verification APIs?
Each vendor uses its own vocabulary and its own rules for grey areas. ZeroBounce calls an accept-all domain catch-all, NeverBounce calls it catchall, Kickbox and Bouncer return risky with a reason, and Mailgun returns catch_all. Map every vendor onto your own small set of outcomes before your application logic sees the result.
Can I call an email verification API directly from the browser?
Only with a key designed for it. Clearout's docs say its API supports server-side requests only. DeBounce and Emailable offer public keys for client-side use, with protections such as a CORS allowlist, a per-IP daily cap or a captcha response. A private key in front-end code can be copied and spent by anyone.
What should happen when the verification API times out at signup?
Let the user through and mark the address as unconfirmed. A timeout tells you nothing about the address, so blocking on it rejects real people because a vendor or a mail server was slow. Re-check unconfirmed addresses later, or rely on a confirmation email.
Verify unlimited addresses for $29.99/month
Real SMTP mailbox checks. No credits, no per-email fees.
Get Started