> Enrich.chat gives AI agents verified work emails and mobiles. To use it from an agent, read https://enrich.chat/agents.md . This is the Markdown version of https://enrich.chat/blog/email-verification-api/

# Email verification API: how to choose one, with response shapes, error codes, limits and billing for 8 vendors

We read eight vendors' API docs on October 16, 2026. See how each labels results, fails, limits and bills, with tested code that treats a status correctly, and where Enrich.chat fits.

By Enrich.chat Editorial. Updated October 19, 2026.

**Short answer:** An email verification API takes one address and returns a verdict such as deliverable, risky or unknown, plus a reason and flags. Across eight vendors read on October 16, 2026, an empty balance shows up as a 402, a 403, a 429 or an error inside a 200, so map labels and errors per vendor. [Enrich.chat](https://enrich.chat/pricing/) is not a verifier: it labels each work email it finds as verified, catch_all or unknown.

## Key takeaways

- Four vendors give catch-all its own verdict word (catch-all, accept_all, catch_all) and three fold it into risky with a flag, so a switch on the verdict alone misses it.
- An empty balance is a 402 at Bouncer and Emailable, a simulated 403 in Kickbox's sandbox, a 429 at Hunter, and an error inside a 200 at MillionVerifier. Hunter's 403 is its rate limit.
- The longest wait you can set runs from 10 seconds at Emailable to 60 seconds at ZeroBounce and MillionVerifier. Hunter holds a call for 20 seconds, then returns 202.
- Only four vendors say what a repeat call costs: Hunter, Emailable, MillionVerifier and Bouncer's batch-sync. None of the verification pages we read mentions an idempotency key.
- Enrich.chat is not a verifier. A work email it finds comes back as verified, catch_all or unknown, and 10,000 credits cover 1,000 of them.

## The verdict

To check addresses you already hold, any of the eight verifiers works, so pick by the call site and the failure signals. **Enrich.chat** is the pick for a different job: new work emails inside an AI agent, each with a verified, catch_all or unknown status. It is not a verifier.

- Best for new work emails in an AI agent: Enrich.chat (A status and credits_used on each work email it finds, not a verifier)
- Best test mode for error paths: Kickbox (Sandbox keys that trigger a timeout and a zero-balance 403)
- Best batch callbacks: Emailable (A callback retried hourly for up to 3 days, plus a retry window on slow checks)
- Most sub-statuses: ZeroBounce (26 sub_status values and 33 sandbox addresses)
- Best fit for an AWS stack: Amazon SES (IAM permissions instead of an API key, and confidence levels you set the threshold on)

## Email verification APIs at a glance

|  | Enrich.chat | Amazon SES | Bouncer | Emailable | Hunter | Kickbox | Mailgun | MillionVerifier | ZeroBounce |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| Key can go in a header | yes: and none for the free tier | yes: AWS credentials | yes: x-api-key | yes: Bearer | yes: header or Bearer | yes: Bearer | yes: HTTP basic | no: query string only | partial: form body on POST |
| Test mode that spends no credits | partial: 10 free lookups a day | no: none on the pages we read | yes: sandbox addresses | yes: test keys | yes: test-api-key | yes: sandbox keys | no: none on the pages we read | yes: demo keys | yes: sandbox addresses |
| Batch endpoint | yes: 25 items a call, or a job of 5,000 | no: one address per call | yes: batch and batch-sync | yes: 50,000 a batch | no: none in the API reference | yes: 1 million a job | yes: CSV up to 25 MB | yes: file upload | yes: batch call and files |
| Callback when a batch ends | yes: signed webhook when a job ends | no | yes: callback parameter | yes: retried for 3 days | no | yes: callback header | no: poll the job | not stated | yes: return_url |
| Rate limit stated as a number | yes: in the API reference | no: 429 only | yes: 1,000 a minute | yes: 25 a second | yes: 10 a second | yes: 8,000 a minute | no: no number | yes: 160 a second | partial: two statements |
| Timeout you can set | no: no latency figure | no | yes: up to 30 s | yes: 2 to 10 s | no: fixed 20 s, then 202 | yes: up to 30 s | no | yes: 2 to 60 s | yes: 3 to 60 s |
| AI agent access | yes: keyless MCP server | no: none seen | no: none seen | partial: CLI and skill | yes: MCP on all plans | no: none seen | no: none seen | no: none seen | yes: official MCP server |

## The 9 APIs: Enrich.chat first, then eight verifiers in alphabetical order

This is not a ranking of accuracy: we ran no verification tests. Enrich.chat is first because it answers a different question, and the eight verifiers follow in alphabetical order. Facts come from each vendor's own docs and pricing pages, read October 16, 2026.

### Enrich.chat

Contact and company data for AI agents and apps. It is not a verifier. GET /v1/email returns a found work email with a verified, catch_all or unknown status and the credits it used.

- Best for: Agents and apps that need new work emails with a status, not a check on a list you already own
- Pricing: Monthly plans from $29: 10 credits per work email found, 0 when nothing is found
- Strength: A status label and credits_used on every found work email
- Strength: A plain GET API with a keyless MCP server; a key is optional
- Strength: JSON errors with a stable code, and a 429 that carries retry_after
- Watch out for: No check for an address you already hold; a batch call takes 25 items, a background job 5,000, and its webhook announces finished jobs only
- Watch out for: Three status values only: no score, reason or disposable and role flags, and no test mode

### Amazon SES

AWS's Email Validation API. GetEmailAddressInsights returns HIGH, MEDIUM or LOW confidence for overall validity and for six individual checks.

- Best for: Teams already on AWS that want IAM permissions and per-address billing
- Pricing: $0.01 for each address validated through the API or console ([Amazon SES pricing](https://aws.amazon.com/ses/pricing/), Oct 16, 2026)
- Strength: Confidence levels instead of labels, so you set the delivery threshold
- Strength: Syntax, DNS, mailbox, role, disposable and random-input checks each have their own level
- Strength: Errors are documented as 400 and 429 exceptions in the [API reference](https://docs.aws.amazon.com/ses/latest/APIReference-V2/API_GetEmailAddressInsights.html)
- Watch out for: No catch-all label or test mode on the pages we read
- Watch out for: One address per call, with no timeout or rate-limit figure published

### Bouncer

Verifier with real-time, batch and batch-sync APIs, an OpenAPI spec and free sandbox addresses.

- Best for: Developers who want a queue-friendly batch-sync endpoint and clear HTTP errors
- Pricing: From $8 for 1,000 credits, which never expire ([Bouncer pricing](https://www.usebouncer.com/pricing/), Oct 16, 2026)
- Strength: Batch-sync keeps results for up to 24 hours, so repeats in that window are not verified again
- Strength: Sandbox addresses cost nothing, and its guide spells out 402, 429 and 503 handling
- Strength: Unknown results and duplicates are not charged, per its pricing page
- Watch out for: Its integration guide describes an hourly real-time limit while the API reference says 1,000 a minute
- Watch out for: Real-time returns about 5% more unknowns than batch, per its reference

### Emailable

Verifier with single and batch endpoints, test keys, public keys for browser widgets, and a CLI and skill for agents that run shell commands.

- Best for: Developers who want a documented retry window, batch callbacks and test keys
- Pricing: From $38 for 5,000 credits, which never expire ([Emailable pricing](https://emailable.com/pricing/), Oct 16, 2026)
- Strength: A timed-out check can be retried for 5 minutes without new usage, and a 249 tells you to resend
- Strength: Batches of up to 50,000 with a callback retried hourly for up to 3 days
- Strength: Rate-limit headers on a 429, and a limits table for each endpoint
- Watch out for: The accept-all check is off by default and slows the call
- Watch out for: Responses can include guessed name, gender and birth-year fields; batches let you ask for fewer

### Hunter

Email finder, verifier and outreach tools behind one API key, with an MCP server on every plan.

- Best for: Teams that find and verify with one key
- Pricing: Verification costs 0.5 credit. Starter is $49 a month for 2,000 credits; Data Platform is $11 for 1,000 verification credits on a 12-month plan ([Hunter pricing](https://hunter.io/pricing), Oct 16, 2026)
- Strength: A slow check returns 202 and you call again, counted once
- Strength: The same verification counts once per billing period, and unknown results use no credit
- Strength: The key goes in the query string, an X-API-KEY header or a Bearer header, and a test-api-key exists
- Watch out for: 429 means usage limit and 403 means rate limit, the reverse of the usual
- Watch out for: No bulk verification endpoint in the API reference we read, and a result can be up to a month old

### Kickbox

Verification platform with single and batch APIs, a Sendex quality score and a sandbox with trigger addresses.

- Best for: Teams that want to test error paths before launch
- Pricing: $5 for 500 verifications and $70 for 10,000 ([Kickbox pricing](https://kickbox.com/pricing/), Oct 16, 2026)
- Strength: Sandbox keys trigger a timeout, a failed connection and a zero-balance 403 on demand
- Strength: Every response carries X-Kickbox-Balance and X-Kickbox-Response-Time headers
- Strength: Batches of up to 1 million addresses, with a callback header
- Watch out for: Verification endpoints block cross-origin calls, so the key must stay on a server
- Watch out for: A sandbox key cannot be switched to a live key

### Mailgun

Sinch Mailgun's Validate API: single and bulk jobs, a result with a risk level and a reason list, bundled into Optimize and Send plans.

- Best for: Teams that already send with Mailgun
- Pricing: Optimize Pilot is $49 a month with 2,500 validations and extras from $1.20 per 100; Starter is $99 with 5,000 and extras from $0.80 per 100 ([Mailgun pricing](https://www.mailgun.com/pricing/), Oct 16, 2026)
- Strength: A result, a risk level and a reason array on every response, plus a provider_lookup switch
- Strength: Bulk jobs by CSV or gzip, with a status endpoint
- Strength: HTTP basic auth with the key as the password
- Watch out for: No test mode, callback or rate-limit number on the pages we read
- Watch out for: Bulk jobs run five at a time, and a sixth gets a 400

### MillionVerifier

Low-priced verifier with a real-time API, a bulk file API and demo keys.

- Best for: Price-sensitive checks where you can handle errors inside a 200 body
- Pricing: $39 for 10,000 credits, which never expire ([MillionVerifier pricing](https://www.millionverifier.com/), Oct 16, 2026)
- Strength: Demo keys simulate each result and each error, including insufficient credits
- Strength: Charges only ok, invalid and disposable results; catch-all and unknown are free
- Strength: A stated 160 requests a second, and an executiontime field in every response
- Watch out for: Errors arrive as result error inside a 200 response
- Watch out for: Every repeat call is charged, and the key goes in the query string

### ZeroBounce

Validation platform with a real-time API, a batch call, file endpoints, a sandbox and an official MCP server.

- Best for: Teams that want detailed sub-statuses and a documented sandbox
- Pricing: Pay as you go from $39 for 2,000 credits; ZeroBounce ONE from $99 a month ([ZeroBounce pricing](https://www.zerobounce.net/pricing), Oct 16, 2026)
- Strength: 26 sub_status values on the validate response, and 33 sandbox addresses to trigger them
- Strength: A POST has been accepted since September 3, 2026, so the key can stay out of the URL
- Strength: Unknown results are never charged
- Watch out for: Its quick start says the API may not be used inside a third-party product without approval
- Watch out for: One error message covers a bad key and an empty balance

## What does an email verification API call look like?

An email verification API takes one address and returns JSON with a verdict, a reason and a few flags. Seven of the eight APIs read on October 16, 2026 take a GET request with the address in the query string. [Amazon SES](https://docs.aws.amazon.com/ses/latest/APIReference-V2/API_GetEmailAddressInsights.html) is the exception: it takes a POST through the AWS SDK or CLI, and an IAM permission replaces the API key.

Every response holds a verdict, a reason, flags for role, free, disposable and catch-all addresses, and sometimes a score. [Bouncer](https://docs.usebouncer.com/api-reference/real-time/verify-email) scores 0 to 100, [Kickbox](https://docs.kickbox.com/docs/single-verification-api) returns a Sendex from 0 to 1, and [Mailgun](https://documentation.mailgun.com/docs/validate/single-valid-ir) returns a risk level.

Vendors offer real-time calls for one address and batch jobs for files. [Bouncer](https://docs.usebouncer.com/api-reference/batch-sync/batch-sync) adds batch-sync, which takes up to 10,000 addresses per request and keeps results for up to 24 hours. Real-time calls trade accuracy for speed: Bouncer's [reference](https://docs.usebouncer.com/api-reference/real-time/verify-email) says they return about 5% more unknown results than its batch API.

*The request path and where the key goes, from each vendor's docs, read October 16, 2026. The address goes in the query parameter shown, and Amazon SES takes it in a JSON body.*

| API | Request | Where the key goes |
| --- | --- | --- |
| [Amazon SES](https://docs.aws.amazon.com/ses/latest/APIReference-V2/API_GetEmailAddressInsights.html) | POST `/v2/email/email-address-insights` | AWS credentials and an IAM permission |
| [Bouncer](https://docs.usebouncer.com/api-reference/real-time/verify-email) | GET `/v1.1/email/verify?email=` | `x-api-key` header, or HTTP basic with the key as the password |
| [Emailable](https://emailable.com/docs/api/emails/) | GET or POST `/v1/verify?email=` | `api_key` parameter or a Bearer header |
| [Hunter](https://hunter.io/api-documentation/v2) | GET `/v2/email-verifier?email=` | `api_key` parameter, `X-API-KEY` header or Bearer header |
| [Kickbox](https://docs.kickbox.com/docs/single-verification-api) | GET `/v2/verify?email=` | `apikey` parameter or a Bearer header |
| [Mailgun](https://documentation.mailgun.com/docs/validate/single-valid-ir) | GET or POST `/v4/address/validate?address=` | HTTP basic, with the user `api` |
| [MillionVerifier](https://developer.millionverifier.com/) | GET `/api/v3/?email=` | `api` query parameter; the docs show no header |
| [ZeroBounce](https://www.zerobounce.net/docs/email-validation-api-quickstart/v2-validate-emails) | GET or POST `/v2/validate?email=` | `api_key` parameter; a POST sends it in a form body |

Encode the address before you send it. [Bouncer's guide](https://docs.usebouncer.com/integration-guidelines) warns that an unencoded plus sign arrives as a space and returns undeliverable. Put the key in a header where the vendor allows one, because URLs end up in logs.

## How do the status labels line up across vendors?

Every verification API reduces its result to four actions: send, hold, retry or drop. The words differ, and so does the field that holds them. Figure 1 maps eight APIs onto those four actions.

**Figure 1: The labels of eight verification APIs mapped to four actions.** A grid with one row per API and four columns: send, hold, retry and drop. ZeroBounce uses valid, catch-all, unknown, and invalid or do_not_mail. Bouncer, Emailable and Kickbox use deliverable, risky, unknown and undeliverable. Hunter uses valid, accept_all or webmail, unknown, and invalid or disposable. Mailgun uses deliverable, catch_all, unknown, and undeliverable or do_not_send. MillionVerifier uses ok, catch_all, unknown, and invalid or disposable. Amazon SES uses HIGH, MEDIUM and LOW confidence with no unknown label.

*Each API's own labels grouped into four actions. The grouping is this guide's suggestion: vendors disagree on disposable addresses, and ZeroBounce also returns spamtrap and abuse, which belong in drop. Labels come from [ZeroBounce](https://www.zerobounce.net/docs/email-validation-api-quickstart/v2-status-codes), [Bouncer](https://docs.usebouncer.com/terminology), [Kickbox](https://docs.kickbox.com/docs/single-verification-api), [Emailable](https://emailable.com/docs/api/emails/), [Hunter](https://hunter.io/api-documentation/v2), [Mailgun](https://documentation.mailgun.com/docs/validate/single-valid-ir), [MillionVerifier](https://developer.millionverifier.com/) and [Amazon SES](https://docs.aws.amazon.com/ses/latest/dg/email-validation-api.html) docs.*

Four vendors give catch-all its own verdict word: ZeroBounce (`catch-all`), Hunter (`accept_all`), and Mailgun and MillionVerifier (`catch_all`). Bouncer, Emailable and Kickbox fold it into `risky` and add a flag. Amazon SES shows no catch-all label on the pages we read. A switch on the verdict alone is not enough, so the table lists the second field to read.

*The field to read next to the verdict, from each vendor's docs, read October 16, 2026.*

| API | Verdict field | Also check |
| --- | --- | --- |
| Amazon SES | `IsValid.ConfidenceVerdict`: HIGH, MEDIUM or LOW | You pick the threshold. Six checks (syntax, DNS, mailbox, role, disposable, random input) each carry their own level. |
| Bouncer | `status` | `reason`, `domain.acceptAll` and `account.fullMailbox`. Its guide says to remove risky results with a full mailbox. |
| Emailable | `state` | `accept_all` comes back null unless you ask for the accept-all check, which is off by default and slows the call. |
| Hunter | `data.status`: six values | `webmail` and `disposable` are statuses, not flags. `verification.date` can be up to a month old. `result` is deprecated. |
| Kickbox | `result` | `reason` has eleven values; `accept_all` and `sendex` are separate fields. |
| Mailgun | `result` | `reason` is an array and is empty on a clean result. `risk` is a second rating. |
| MillionVerifier | `result` plus `resultcode` 1 to 6 | `result` can be `error` inside a 200 response. `subresult` holds the cause. |
| ZeroBounce | `status`: seven values | `sub_status`. A valid result can carry `accept_all`, which means a catch-all domain on its allow-list. |

Keep the raw label next to your action, so you can re-map it later. Send any label your table does not know to retry, never to send, because vendors add labels. Our guide to [bulk email verification](https://enrich.chat/blog/bulk-email-verification/) explains what to do with each pile, and the free [catch-all checker](https://enrich.chat/tools/catch-all-checker/) tests a domain.

## What do the speed and rate-limit claims say?

Every speed figure is the vendor's own, and none comes from an independent test. The number that matters for your code is the longest wait the docs allow, because your HTTP client has to wait longer than that. Figure 2 charts it.

**Figure 2: Default and longest wait per verification check, in seconds.** A bar chart of the wait each API documents for one check. ZeroBounce defaults to 30 seconds and allows 60. MillionVerifier defaults to 20 and allows 60. Bouncer defaults to 10 and allows 30. Kickbox defaults to 6 and allows 30. Hunter holds a call for 20 seconds, then returns 202. Emailable defaults to 5 and allows 10, then returns 249. Mailgun and Amazon SES publish no timeout figure on the pages we read.

*The wait each API documents for one check, in seconds. The solid bar is the default and the dashed outline is the longest setting; Hunter holds a call for a fixed 20 seconds before it returns 202. These are vendor settings, not measured speeds. Sources: [ZeroBounce](https://www.zerobounce.net/docs/email-validation-api-quickstart/v2-validate-emails), [MillionVerifier](https://developer.millionverifier.com/), [Bouncer](https://docs.usebouncer.com/api-reference/real-time/verify-email), [Kickbox](https://docs.kickbox.com/docs/single-verification-api), [Hunter](https://hunter.io/api-documentation/v2) and [Emailable](https://emailable.com/docs/api/emails/).*

The claims differ in kind. [MillionVerifier](https://help.millionverifier.com/email-verification-api/api-speed) says it can verify an address in under 100 milliseconds, usually in under 0.5 seconds, and that some checks take over 20 seconds. [ZeroBounce](https://www.zerobounce.net/docs/email-validation-api-quickstart/v2-validate-emails) says responses take 1 to 30 seconds and that 96% to 98% of domains return in 1 to 5 seconds. [Kickbox](https://docs.kickbox.com/docs/using-the-api) adds an `X-Kickbox-Response-Time` header, so you can log the vendor's side of each wait. Mailgun and Amazon SES give no figure on the pages we read.

*Rate limits as each vendor states them, read October 16, 2026. Numbers are the vendors' own.*

| API | Limit as stated | Note |
| --- | --- | --- |
| Amazon SES | No number; a 429 `TooManyRequestsException` | Listed in the [API reference](https://docs.aws.amazon.com/ses/latest/APIReference-V2/API_GetEmailAddressInsights.html) |
| Bouncer | 1,000 requests a minute by default for real-time; 60 batches a minute; 100 batch-sync requests a minute | Its integration guide describes an hourly limit instead |
| Emailable | 25 a second on verify, 5 a second on batch | 429 comes with `ratelimit-limit`, `ratelimit-remaining` and `ratelimit-reset` headers ([limits](https://emailable.com/docs/api/rate-limits/)) |
| Hunter | 10 a second and 300 a minute on the verifier | Stated on the [verifier section](https://hunter.io/api-documentation/v2) |
| Kickbox | 25 requests in flight per IP, and 8,000 a clock minute | A 429 for too many in flight can be retried at once |
| Mailgun | "A set number of active requests at a time", with no number | On a 429, wait and try again |
| MillionVerifier | 160 requests a second per account | Tied to the account, per its [help page](https://help.millionverifier.com/email-verification-api/real-time-api) |
| ZeroBounce | 80,000 requests in 10 seconds (100,000 on ZeroBounce ONE), then a 1-minute block | The same page also says the endpoint is not currently rate-limited |

When two pages from one vendor disagree, as Bouncer's and ZeroBounce's do, plan for the stricter reading and ask support.

## How should your code handle errors, timeouts and retries?

Treat a timeout as unknown, never as invalid, and read each vendor's own signals for an empty balance and a rate limit, because they do not agree. The table sets the signals side by side.

*How each API signals trouble, from its docs, read October 16, 2026.*

| API | Out of credits | Rate limited | Slow or unfinished check |
| --- | --- | --- | --- |
| Amazon SES | No balance: billed per validation | 429 | No timeout stated; bad input gets a 400 |
| Bouncer | 402 | 429 | `unknown` with reason `timeout`; `retryAfter` after greylisting |
| Emailable | 402 | 429 with limit headers | 249: send the same request again; retries inside 5 minutes are not billed twice |
| Hunter | **429**, the usage limit | **403** | 202: call again; it counts as one request |
| Kickbox | Balance depleted; its sandbox simulates an HTTP 403 | 429 | `unknown` with reason `timeout` or `no_connect` |
| Mailgun | Not on the page we read | 429 | The `reason` array can say `smtp_timeout` |
| MillionVerifier | `result` is `error`; a demo key simulates it | Code not stated | `subresult` can say `timeout_error` or `connection_timeout` |
| ZeroBounce | An error body; one message covers a bad key and an empty balance | A temporary block | `unknown` with `greylisted` or `timeout_exceeded` |

Hunter's pair is the reverse of the usual convention. Its [docs](https://hunter.io/api-documentation/v2) say 429 is the usage limit and 403 is the rate limit. Retry a 403 after a pause, and never retry a 429, because waiting will not refill a balance. ZeroBounce returns one message for a bad key and an empty balance, so a retry loop can spin on either. For unknown results, retry once after a delay and then hold the address. [ZeroBounce](https://www.zerobounce.net/docs/email-validation-api-quickstart/v2-status-codes) says greylisted addresses often validate on a second pass.

The sketch below puts those rules in one place. It maps the labels of eight vendors, stops on an empty balance, waits on rate limits and caches only final answers. Amazon SES goes through the AWS SDK, so the sketch maps only its response.

```
import time

import requests

# Per vendor: the path to the verdict, and a label-to-action map.
SAME = {"deliverable": "send", "risky": "hold",
        "unknown": "retry", "undeliverable": "drop"}
RULES = {
    "bouncer": ("status", SAME),
    "emailable": ("state", SAME),
    "kickbox": ("result", SAME),
    "mailgun": ("result", {
        "deliverable": "send", "catch_all": "hold", "unknown": "retry",
        "undeliverable": "drop", "do_not_send": "drop"}),
    "zerobounce": ("status", {
        "valid": "send", "catch-all": "hold", "unknown": "retry",
        "invalid": "drop", "do_not_mail": "drop",
        "spamtrap": "drop", "abuse": "drop"}),
    "hunter": ("data.status", {
        "valid": "send", "accept_all": "hold", "webmail": "hold",
        "unknown": "retry", "invalid": "drop", "disposable": "drop"}),
    "millionverifier": ("result", {
        "ok": "send", "catch_all": "hold", "unknown": "retry",
        "invalid": "drop", "disposable": "drop"}),
    "ses": ("MailboxValidation.IsValid.ConfidenceVerdict", {
        "HIGH": "send", "MEDIUM": "hold", "LOW": "drop"}),
}
# An empty balance: stop, do not retry.
STOP = {"bouncer": {402}, "emailable": {402}, "kickbox": {403},
        "hunter": {429}}
# Wait and retry. Hunter's 403 is its rate limit.
WAIT = {"emailable": {249, 429, 503}, "hunter": {202, 403, 503}}
WAIT_DEFAULT = {429, 503}
CACHE = {}


def dig(obj, path):
    for key in path.split("."):
        obj = obj.get(key) if isinstance(obj, dict) else None
    return obj


def action(vendor, body):
    path, labels = RULES[vendor]
    # A label you have not seen is never "send".
    return labels.get(dig(body, path), "retry")


def check(vendor, url, params, tries=4, pause=1.0):
    for attempt in range(tries):
        r = requests.get(url, params=params, timeout=35)
        if r.status_code in STOP.get(vendor, ()):
            raise RuntimeError(vendor + ": out of credits, stop")
        if r.status_code in WAIT.get(vendor, WAIT_DEFAULT):
            time.sleep(pause * (attempt + 1))
            continue
        body = r.json()
        if (body.get("error") or body.get("success") is False
                or body.get("result") == "error"):
            raise RuntimeError(vendor + ": " + str(body))
        return {"action": action(vendor, body), "raw": body}
    return {"action": "retry", "raw": None}


def verify(vendor, url, params, address, ttl=90 * 86400):
    key = (vendor, address.strip().lower())
    hit = CACHE.get(key)
    if hit and time.time() - hit["at"] < ttl:
        return hit
    result = check(vendor, url, params)
    if result["action"] != "retry":  # a retry still needs a re-check
        CACHE[key] = dict(result, at=time.time())
    return result
```

We ran this code against a stand-in server on localhost that returns the field names above, never against a vendor or a real mailbox. The addresses are fictional and sit on example.com.

*What the sketch did against the stand-in server.*

| The stand-in answered | The sketch |
| --- | --- |
| Emailable-style 249, twice, then 200 deliverable | Waited 1 s, then 2 s, then returned send |
| Hunter-style 403, then 200 valid | Waited 1 s, retried, returned send |
| Hunter-style 202, then 200 valid | Waited 1 s, called again, returned send |
| Hunter-style 429 | Stopped without a retry |
| Bouncer-style 402 or Kickbox-style 403 | Stopped without a retry |
| MillionVerifier-style 200 with result error | Stopped and raised the error body |
| ZeroBounce-style unknown, greylisted | Returned retry and did not cache it |
| A repeat of a cached valid address | One request for two calls |
| A label the table does not know | Returned retry, not send |

## What does a repeat call cost, and how should you cache results?

Four vendors say what a repeat call costs: Hunter, Emailable, MillionVerifier and Bouncer's batch-sync endpoint. None of the verification pages we read mentions an idempotency key, so keep your own cache. Hunter documents an `Idempotency-Key` header for creating sequences, not for its verifier.

*How a call is billed and what a repeat costs, from each vendor's own pages, read October 16, 2026.*

| API | How a call is billed | Repeat or retry rule |
| --- | --- | --- |
| Amazon SES | $0.01 for each address validated through the API or console ([pricing](https://aws.amazon.com/ses/pricing/)) | None stated |
| Bouncer | Credits; unknown results and duplicates are not charged ([pricing](https://www.usebouncer.com/pricing/)) | Batch-sync keeps results for up to 24 hours, then verifies and charges again |
| Emailable | 1 credit per verification; unknown results are credited back ([pricing](https://emailable.com/pricing/)) | Retry a timed-out check for up to 5 minutes without new usage |
| Hunter | 0.5 credit per verification; none when the result is unknown ([pricing](https://hunter.io/pricing)) | The same verification counts once per billing period; polling a 202 counts once |
| Kickbox | Credits; unknown results are refunded ([pricing](https://kickbox.com/pricing/)) | None stated |
| Mailgun | An allowance inside a plan, then a price per 100 validations ([pricing](https://www.mailgun.com/pricing/)) | None stated |
| MillionVerifier | Credits for ok, invalid and disposable results; catch-all and unknown are free ([help page](https://help.millionverifier.com/email-verification/email-verification-results)) | Every repeat call is charged, because it cannot tell whether an address was verified before |
| ZeroBounce | 1 credit per validation; unknown results are never charged ([pricing](https://www.zerobounce.net/pricing)) | None stated for single calls |

The prices behind those models are in our guide to [email verification tools](https://enrich.chat/blog/email-verification-tools/). Most vendors sell credit packs. Amazon SES bills per address. [Mailgun](https://www.mailgun.com/pricing/) bundles an allowance into a plan and charges overage per 100 validations. Abstract API, which is not in the tables, counts every request as a credit, even for a malformed address, per its [docs](https://docs.abstractapi.com/api/email-validation).

Cache final answers by lowercase address and vendor, with the check date. Vendors suggest re-checking addresses older than 90 days, as our [bulk verification guide](https://enrich.chat/blog/bulk-email-verification/) explains, and [B2B data decay](https://enrich.chat/blog/b2b-data-decay/) shows why work emails age quickly. Never cache a retry result.

## How do batch jobs, callbacks and test modes work?

Use a batch endpoint for more than a few hundred addresses, and a callback if you do not want to poll. Six of the eight vendors document a test mode.

### Which APIs offer batch jobs and callbacks?

*Batch limits and how you learn a job finished, from each vendor's docs, read October 16, 2026.*

| API | Batch limits | When it finishes |
| --- | --- | --- |
| Amazon SES | One address per call in the API reference | Not applicable |
| [Bouncer](https://docs.usebouncer.com/api-reference/batch/batch-create) | Up to 100,000 emails a batch, with 1,000 to 10,000 recommended; batch-sync takes 10,000 a request | Poll the status, or pass a callback URL |
| [Emailable](https://emailable.com/docs/api/emails/) | Up to 50,000 emails a batch | A `url` parameter receives a POST, retried hourly until a 200 or for 3 days |
| Hunter | No bulk verification in the API reference we read; the app has a bulk verifier | Not applicable |
| [Kickbox](https://docs.kickbox.com/docs/batch-verification-api) | Up to 1 million addresses and 250 MB a job | An `X-Kickbox-Callback` header; the POST carries stats and a download link that lasts one hour |
| [Mailgun](https://documentation.mailgun.com/docs/validate/bulk-valid-ir) | CSV or gzip up to 25 MB; five jobs at a time, and a sixth gets a 400 | Poll the job; no callback on the page we read |
| MillionVerifier | File upload through the bulk API | Poll the file info; no callback on the page we read |
| [ZeroBounce](https://www.zerobounce.net/docs/email-validation-api-quickstart/v2-batch-validate-emails) | A batch call that can take up to 70 seconds; larger jobs go to its file endpoints | A `return_url` on file upload is called when the file is done |

Make your callback endpoint return a 2xx quickly, then fetch the results by job id, so a replayed callback does no harm. Emailable keeps retrying every hour until it gets a 200.

### How do you test without spending credits?

*Test inputs documented by each vendor, read October 16, 2026. Amazon SES and Mailgun document none.*

| API | Test mode | What you can trigger |
| --- | --- | --- |
| [Bouncer](https://docs.usebouncer.com/sandbox) | Sandbox addresses on `sandbox.usebouncer.com`, such as `deliverable@` | Deliverable, free, disposable, accept-all, undeliverable, unknown and other |
| [Emailable](https://emailable.com/docs/api/emails/) | A key that starts with `test_`, with addresses such as `risky@example.com` | Each state; `slow@` returns 249; a `simulate` value fakes batch errors |
| [Hunter](https://hunter.io/api-documentation/v2) | The key `test-api-key` | One dummy response every time, on the verifier, finder and domain search |
| [Kickbox](https://docs.kickbox.com/docs/sandbox-api) | A `test_` sandbox key, which cannot be switched to a live key | Local parts such as `timeout@` and `no-connect@`; `insufficient-balance@` returns a 403 |
| [MillionVerifier](https://developer.millionverifier.com/) | Demo keys such as `API_KEY_FOR_OK` | Each result, and errors, including an insufficient-credits key |
| [ZeroBounce](https://www.zerobounce.net/docs/email-validation-api-quickstart/v2-sandbox-mode) | 33 sandbox addresses, used with your real key | Each status and many sub-statuses, such as `greylisted@` |

Test addresses prove your code reads the fields. They do not prove accuracy, because the results are fixed. For the two vendors with no test mode, and for continuous integration, run a stand-in server like ours, so your tests never reach a vendor or a real mailbox.

## What should you store from a verification response?

Store the vendor's raw verdict and reason, your own action, the check date and the vendor name, and nothing you will not use. That is enough to re-map labels later and to know which rows are stale.

- **The catch-all flag.** It is a separate field at several vendors, as the table above shows.
- **The check date.** Use the vendor's timestamp where it gives one: ZeroBounce returns `processed_at`, and Hunter returns `verification.date`, which can be a month old.
- **The mode.** Real-time results can hold more unknowns than batch results.
- **The job id and the credits.** Bouncer returns a `batchId`, Emailable an `id`, and Kickbox a balance in the `X-Kickbox-Balance` header.

Log the address as you sent it, too. [Kickbox](https://docs.kickbox.com/docs/single-verification-api) returns a normalized address and says it is functionally equivalent, but advises logging the original. Some responses also add guesses about a person: [Emailable](https://emailable.com/docs/api/emails/) can return a possible first name, gender and birth year, and ZeroBounce can return a first name, last name and gender. Drop fields you do not need before you store the row. This is not legal advice, and our [cold email data guide](https://enrich.chat/blog/cold-email/) covers the rules for emailing people.

## How does Enrich.chat handle email verification through its API?

Enrich.chat is not a standalone verifier. You cannot send it an address from another tool and get a deliverability check. It labels each work email it finds: both `GET /v1/email` and `GET /v1/person` return a `status` of `verified`, `catch_all` or `unknown`. The label is not a delivery guarantee, so treat every contact as a strong lead.

Here is a lookup for a fictional person on example.com. The response is illustrative.

```
GET https://enrich.chat/v1/email?name=Lena%20Marsh&domain=example.com

{
  "found": true,
  "work_email": {"value": "lena.marsh@example.com", "status": "verified"},
  "pattern": "first.last@example.com",
  "name": "Lena Marsh",
  "title": "Head of Procurement",
  "credits_used": 10,
  "free_lookups_remaining": 9
}
```

The free tier needs no key, and a key is optional: once you pay for a plan, send it as `Authorization: Bearer pp_...` or `?key=pp_...`. Errors are JSON with a stable `error` code. A 402 means `free_limit_reached`, `credits_exhausted` or `plan_inactive`, so stop. A 429 carries `retry_after`, so wait. A 503 uses nothing and gets one retry. Unlike Hunter, 429 here is the rate limit and 402 is the empty balance. A miss returns `{"found": false}`, uses 0 credits and may include a `guess`, which is a hint and not data.

**Figure 3: A lookup loop that handles each Enrich.chat response.** One row at a time goes to GET /v1/email. A 200 with a verified status goes to the send group. A 200 with catch_all or unknown goes to the hold group. A 200 with found false goes to the skip group and any guess is ignored. A 429 waits retry_after seconds and retries the row. A 402 stops the loop and hands back the unfinished rows. Every 200 adds credits_used to a running total, at about one request a second.

*The loop in the code below. The response shapes and error codes follow [agents.md](https://enrich.chat/agents.md). A 503 gets one retry after retry_after seconds and uses nothing.*

The loop below follows Figure 3. It sends rows one at a time at about one request a second, sorts them into three groups and stops on a 402. Any status it does not know goes to hold.

```
import time

import requests

URL = "https://enrich.chat/v1/email"  # the test run used a stand-in


def run(rows, key=None, pace=1.0):
    groups = {"send": [], "hold": [], "skip": []}
    credits = 0
    headers = {"Authorization": "Bearer " + key} if key else {}
    for i, row in enumerate(rows):
        params = {"name": row["name"], "domain": row["domain"]}
        for attempt in range(2):
            r = requests.get(URL, params=params, headers=headers,
                             timeout=30)
            if r.status_code not in (429, 503):
                break
            # Wait as told, then retry this row once.
            time.sleep(r.json().get("retry_after", 1))
        body = r.json()
        if r.status_code == 402:  # free limit, credits or plan: stop
            return groups, credits, rows[i:], body.get("upgrade_url")
        r.raise_for_status()
        credits += body.get("credits_used", 0)
        email = body.get("work_email")
        if not body["found"] or not email:
            groups["skip"].append(row)  # a guess is a hint, not data
        elif email["status"] == "verified":
            groups["send"].append(dict(row, email=email["value"]))
        else:  # catch_all, unknown, or a label added later
            groups["hold"].append(
                dict(row, email=email["value"], status=email["status"]))
        time.sleep(pace)  # about one request a second
    return groups, credits, [], None
```

We ran it against a stand-in server on localhost, never the real API, with seven fictional rows. Three landed in send, two in hold (one catch_all, one unknown) and one was skipped with its guess ignored. One 429 was waited out. The seventh row got a 402 and came back with the upgrade link. The five found emails added up to 50 credits.

A work email found uses 10 credits. A list of 20,000 names needs 200,000 credits if every one is found, which fits the Scale plan at $90 a month with unlimited seats. Every miss uses 0 and lowers the total. See the [pricing page](https://enrich.chat/pricing/) for plan terms.

Know the gaps before you build on it:

- No endpoint checks an address you already hold. The email input on `/v1/person` finds the person behind a work email, and personal mailboxes such as Gmail are not looked up.
- There is no test mode on the self-serve plans, and 10 lookups a day are the closest thing to a sandbox. `POST /v1/batch` takes up to 25 items per call and returns every result in the reply. For up to 5,000 rows, start a [background job](https://enrich.chat/features/background-jobs/) with `POST /v1/jobs` or the dashboard's Run it on the server option. It keeps going after you close the tab, and a signed webhook tells your app when it finishes or stops. The webhook announces finished jobs only, not single lookups. See [bulk enrichment](https://enrich.chat/features/bulk-enrichment/).
- The status has three values: no score, reason code, or disposable, role or typo flag.
- A search with `with_email=true` returns the email with the status `unknown`. Call `/v1/email` or `/v1/person` when you need the status.
- The company must be a website such as example.com, not a name.

```text
Use www.enrich.chat to find the work email for each person in this list from their name and company website: [paste rows]. Show the email exactly as returned, its status and credits_used. Put verified rows in a send group, catch_all and unknown rows in a hold group for my own verifier, and not found rows in a skip group. Never guess an address. On a 429 wait retry_after seconds. On a 402 stop and give me the upgrade link and the rows left.
```

The [email finder API](https://enrich.chat/api/email-finder/) page covers the endpoint, the free [email finder](https://enrich.chat/tools/email-finder/) works in a browser, and the [install page](https://enrich.chat/install/) has steps for ChatGPT, Claude, Cursor and Codex. Your agent gets 10 free lookups a day with no signup, then monthly plans start at $29.

## How do you choose, and when is another API the better choice?

Choose by the call site and the failure signals, not by the label names. Test two APIs on 100 to 200 addresses you know before you buy credits, as our guide to [email verification tools](https://enrich.chat/blog/email-verification-tools/) suggests.

### What is a buying checklist for an email verification API?

1. **Name the call site.** A sign-up form needs a real-time call and a timeout you control. An import needs a batch job and a callback. An agent loop needs steady pacing and clear stop signals.
2. **Test the three failure signals.** Empty balance, rate limit and slow check, from the table above.
3. **Check what a repeat call costs.** If the vendor says nothing, assume every call is billed and cache on your side.
4. **Check the test mode.** Your tests should run without credits and without real mailboxes.
5. **Check who may call and from where.** ZeroBounce's [quick start](https://www.zerobounce.net/docs/email-validation-api-quickstart/) says its API may not be used inside a third-party product without approval. Kickbox's [docs](https://docs.kickbox.com/docs/using-the-api) say its verification endpoints block cross-origin calls, so the key stays on a server. Emailable offers public keys for browser widgets, limited to the verify endpoint and your trusted domains.

### When is another API the better choice than Enrich.chat?

Another API is the better choice whenever the address already exists in your files, or you need bulk verification of a file, a callback for a verification batch or a test mode.

- **Amazon SES** when your stack is on AWS and you want IAM permissions and confidence levels. See its [validation guide](https://docs.aws.amazon.com/ses/latest/dg/email-validation-api.html).
- **Bouncer** when you want a queue-friendly batch-sync endpoint. See its [integration guidelines](https://docs.usebouncer.com/integration-guidelines).
- **Emailable** when you want a retry window, batch callbacks and test keys. See its [API reference](https://emailable.com/docs/api/emails/).
- **Hunter** when you want to find and verify with one key. See [Hunter pricing](https://enrich.chat/blog/hunter-io-pricing/) and [API docs](https://hunter.io/api-documentation/v2).
- **Kickbox** when you want a sandbox that triggers a timeout and a zero balance. See its [sandbox page](https://docs.kickbox.com/docs/sandbox-api).
- **Mailgun** when you already send with Mailgun. See its [validate docs](https://documentation.mailgun.com/docs/validate/single-valid-ir).
- **MillionVerifier** when price matters most and you can handle errors inside a 200 body. See its [developer docs](https://developer.millionverifier.com/).
- **ZeroBounce** when you want detailed sub-statuses and an official MCP server. See its [MCP server docs](https://www.zerobounce.net/docs/api-integrations/zerobounce-mcp-server) and our [ZeroBounce pricing guide](https://enrich.chat/blog/zerobounce-pricing/).

Choose Enrich.chat when you start from names and company websites, not from addresses. It finds new work emails and does not verify old ones, and it has no test mode, extension or sequencer, so pair it with a verifier for old lists and a sender for outreach, and watch your [bounce rate](https://enrich.chat/blog/email-bounce-rate/).

## Frequently asked questions

### What does an email verification API return?

A JSON body with a verdict, a reason and flags. The verdict field is `status` at [ZeroBounce](https://www.zerobounce.net/docs/email-validation-api-quickstart/v2-validate-emails), [Bouncer](https://docs.usebouncer.com/api-reference/real-time/verify-email) and [Hunter](https://hunter.io/api-documentation/v2), `result` at [Kickbox](https://docs.kickbox.com/docs/single-verification-api), [Mailgun](https://documentation.mailgun.com/docs/validate/single-valid-ir) and [MillionVerifier](https://developer.millionverifier.com/), `state` at [Emailable](https://emailable.com/docs/api/emails/), and a HIGH, MEDIUM or LOW confidence at [Amazon SES](https://docs.aws.amazon.com/ses/latest/dg/email-validation-api.html). Map every label to send, hold, retry or drop in one function, and keep the raw label. See [email validation](https://enrich.chat/blog/email-validation/) for the checks behind it.

### How fast is an email verification API?

Every figure is the vendor's own. [MillionVerifier](https://help.millionverifier.com/email-verification-api/api-speed) says it can verify in under 100 milliseconds and usually under 0.5 seconds, but that some servers take over 20. [ZeroBounce](https://www.zerobounce.net/docs/email-validation-api-quickstart/v2-validate-emails) says responses take 1 to 30 seconds. [Hunter](https://hunter.io/api-documentation/v2) holds a call for 20 seconds before it returns 202. Set your client timeout above the vendor's longest setting, and treat a timeout as unknown, not invalid.

### What should you do when a verification API returns unknown?

Do not treat it as invalid. Retry once later, because [ZeroBounce](https://www.zerobounce.net/docs/email-validation-api-quickstart/v2-status-codes) says greylisted addresses often validate on a second pass, and Bouncer adds a `retryAfter` time when it can offer a result after greylisting. If the address stays unknown, hold it back from sends. Do not cache an unknown, and check your vendor's billing rules in our guide to [email verification tools](https://enrich.chat/blog/email-verification-tools/).

### How do you test an email verification API without spending credits?

Six of the eight APIs we read document a test mode: [Bouncer](https://docs.usebouncer.com/sandbox) sandbox addresses, test keys at [Emailable](https://emailable.com/docs/api/authentication/) and [Kickbox](https://docs.kickbox.com/docs/sandbox-api), [Hunter](https://hunter.io/api-documentation/v2)'s `test-api-key`, [MillionVerifier](https://developer.millionverifier.com/) demo keys and [ZeroBounce](https://www.zerobounce.net/docs/email-validation-api-quickstart/v2-sandbox-mode) sandbox addresses. Amazon SES and Mailgun document none, so run a stand-in server in your tests. Fixed test results prove your code reads the fields, not that the vendor is accurate.

### How do verification APIs report an empty balance or a rate limit?

They disagree. [Bouncer](https://docs.usebouncer.com/api-reference/real-time/verify-email) and [Emailable](https://emailable.com/docs/api/status-codes/) use 402 for an empty balance and 429 for a rate limit. Kickbox's [sandbox](https://docs.kickbox.com/docs/sandbox-api) simulates a zero balance as a 403. [Hunter](https://hunter.io/api-documentation/v2) uses 429 for its usage limit and 403 for its rate limit, the reverse. MillionVerifier reports errors inside a 200 response. Read each vendor's table and stop retrying on an empty balance.

### Can Enrich.chat verify an email address through its API?

No. Enrich.chat has no endpoint that tests an address you already hold. When `/v1/email` or `/v1/person` finds a work email, the response carries a status of verified, catch_all or unknown, which is a label and not a delivery guarantee. A lookup that finds nothing uses 0 credits. See the [email finder API](https://enrich.chat/api/email-finder/) and the free [catch-all checker](https://enrich.chat/tools/catch-all-checker/).

## Sources

1. [Amazon SES: Email Validation API](https://docs.aws.amazon.com/ses/latest/dg/email-validation-api.html)
2. [Amazon SES API v2: GetEmailAddressInsights](https://docs.aws.amazon.com/ses/latest/APIReference-V2/API_GetEmailAddressInsights.html)
3. [Amazon SES pricing](https://aws.amazon.com/ses/pricing/)
4. [Bouncer integration guidelines](https://docs.usebouncer.com/integration-guidelines)
5. [Bouncer quick start](https://docs.usebouncer.com/quick-start)
6. [Bouncer: verify email (real-time)](https://docs.usebouncer.com/api-reference/real-time/verify-email)
7. [Bouncer: create batch request](https://docs.usebouncer.com/api-reference/batch/batch-create)
8. [Bouncer: process with email verification queue (batch-sync)](https://docs.usebouncer.com/api-reference/batch-sync/batch-sync)
9. [Bouncer sandbox](https://docs.usebouncer.com/sandbox)
10. [Bouncer pricing](https://www.usebouncer.com/pricing/)
11. [Emailable API: emails](https://emailable.com/docs/api/emails/)
12. [Emailable API: authentication](https://emailable.com/docs/api/authentication/)
13. [Emailable API: rate limits](https://emailable.com/docs/api/rate-limits/)
14. [Emailable API: status codes](https://emailable.com/docs/api/status-codes/)
15. [Emailable pricing](https://emailable.com/pricing/)
16. [Hunter API documentation](https://hunter.io/api-documentation/v2)
17. [Hunter plans and pricing](https://hunter.io/pricing)
18. [Data Platform plans for API users](https://help.hunter.io/en/articles/9920427-data-platform-plans-for-api-users)
19. [Kickbox API quickstart](https://docs.kickbox.com/docs/using-the-api)
20. [Kickbox single verification API](https://docs.kickbox.com/docs/single-verification-api)
21. [Kickbox batch verification API](https://docs.kickbox.com/docs/batch-verification-api)
22. [Kickbox sandbox mode](https://docs.kickbox.com/docs/sandbox-api)
23. [Kickbox pricing](https://kickbox.com/pricing/)
24. [Mailgun single validation](https://documentation.mailgun.com/docs/validate/single-valid-ir)
25. [Mailgun bulk validation](https://documentation.mailgun.com/docs/validate/bulk-valid-ir)
26. [Mailgun pricing](https://www.mailgun.com/pricing/)
27. [MillionVerifier API documentation](https://developer.millionverifier.com/)
28. [MillionVerifier real-time API](https://help.millionverifier.com/email-verification-api/real-time-api)
29. [MillionVerifier API speed](https://help.millionverifier.com/email-verification-api/api-speed)
30. [MillionVerifier email verification results](https://help.millionverifier.com/email-verification/email-verification-results)
31. [MillionVerifier pricing](https://www.millionverifier.com/)
32. [ZeroBounce single email validator (v2)](https://www.zerobounce.net/docs/email-validation-api-quickstart/v2-validate-emails)
33. [ZeroBounce batch email validator (v2)](https://www.zerobounce.net/docs/email-validation-api-quickstart/v2-batch-validate-emails)
34. [ZeroBounce status codes (v2)](https://www.zerobounce.net/docs/email-validation-api-quickstart/v2-status-codes)
35. [ZeroBounce sandbox mode (v2)](https://www.zerobounce.net/docs/email-validation-api-quickstart/v2-sandbox-mode)
36. [ZeroBounce email validation quickstart](https://www.zerobounce.net/docs/email-validation-api-quickstart/)
37. [ZeroBounce MCP server](https://www.zerobounce.net/docs/api-integrations/zerobounce-mcp-server)
38. [ZeroBounce pricing](https://www.zerobounce.net/pricing)
39. [Abstract API: Email Validation API](https://docs.abstractapi.com/api/email-validation)

## Try this prompt

Find work emails and hold back the risky ones

```text
Use www.enrich.chat to find the work email for each person in this list from their name and company website: [paste rows]. Show the email exactly as returned, its status and credits_used. Put verified rows in a send group, catch_all and unknown rows in a hold group for my own verifier, and not found rows in a skip group. Never guess an address. On a 429 wait retry_after seconds. On a 402 stop and give me the upgrade link and the rows left.
```

## Related

- [Best email verification tools (2026): 9 compared on price, limits and AI agent access](https://enrich.chat/blog/email-verification-tools/)
- [Email validation explained](https://enrich.chat/blog/email-validation/)
- [Email finder API](https://enrich.chat/api/email-finder/)
