On this page

Developer documentation

API Guide

One REST call runs 28 checks on an email address and returns a score, a risk verdict, and the evidence behind it, usually in under a second. Verification is a conversation with the recipient's mail server that stops before any message is sent, so no email is ever delivered to the addresses you check.

Base URL

https://verifications.email

Authentication

X-API-Key: eva_your_api_key

Quickstart

Three steps from zero to a verified address.

1. Create an account and an API key

Sign up (300 free verifications a month, no credit card), then open API Keys in the dashboard and create a key. Keys start with eva_ and are shown once, so store it in your environment rather than in code.

2. Verify your first address

curl "https://verifications.email/api/v1/verify?email=user@gmail.com" \
  -H "X-API-Key: $EVA_API_KEY"

3. Read the result

Every response is wrapped in a data envelope. The two fields most integrations act on are risk and score; the rest is the evidence behind them.

response · 200 OK
{
  "data": {
    "email": "user@gmail.com",
    "score": 94,
    "risk": "safe",
    "syntax_valid": true,
    "mx_found": true,
    "smtp_check": "deliverable",
    "is_disposable": false,
    "is_role_account": false,
    "is_catch_all": false,
    "is_free_provider": true,
    "is_b2b": false,
    "spam_trap_risk": "low",
    "has_sub_addressing": false,
    "suggested_correction": null,
    "smtp_provider": "google",
    "deliverability": {
      "grade": "A",
      "score": 92,
      "authentication": { "spf": "strict", "dkim": "found", "dmarc": "reject" },
      "blacklist": { "listed": false, "listed_count": 0, "high_confidence_count": 0, "total_checks": 10, "evaluated_checks": 10 }
    }
  }
}

See Understanding results for what each field means and what to do with each verdict. The reference lists every field.

Authentication

Every request carries your API key in the X-API-Key header. Keys are the string eva_ followed by 64 hexadecimal characters. Create, name, and revoke keys under API Keys in the dashboard; a revoked key stops working immediately.

request header
X-API-Key: eva_3f9c2a1b8e7d4c6f0a9b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f

Understanding results

A result has two verdict fields and a set of evidence fields. Start with risk; use the rest to decide how much to trust it.

Verdicts

FieldValuesMeaning
risksafe · risky · catchall · invalid · unknown safe is a score of 70 to 100, risky 40 to 69, invalid 0 to 39. catchall sits in the same 40 to 69 band: the domain accepts mail for every address, so the mailbox itself could not be confirmed. unknown means the mail server gave no definitive answer (greylisting, a timeout, a temporary refusal). Unknown results are never billed on any plan.
score0 to 100Composite confidence that mail to this address will be accepted. Built from the SMTP result, provider signals, catch-all scoring, spam-trap risk, and domain health.
smtp_checkdeliverable · undeliverable · risky · unknown What the recipient's mail server said about the mailbox. risky covers catch-all and accept-all domains, full inboxes, and overloaded servers: the address exists but mail to it can still bounce. undeliverable also covers domains that take no mail at all: a null MX record, or a mail service that has shut down, such as voila.fr, which closed in 2016. EVA recognises those without probing.
reasoncode, empty for a plain deliverable Why the verdict landed where it did. Dead domain: no_mx, null_mx, loopback_mx, dead_mx, invalid_mx, discontinued, relay_denied, dns_lame (a lame delegation is invalid once confirmed on a second resolver, unknown while unconfirmed). Mailbox answer: mailbox_not_found, disabled, syntax_invalid. Catch-all band: catch_all, accept_all. Risky cause: full_inbox, mailbox_busy, account_recovery (the provider flagged the account for a forced password reset; the address exists, deliverability unproven), watchlist_typo_trap, disposable (an address on the disposable list, whatever its server said). Inconclusive, never billed: greylisted, rate_limited, temp_failure, ip_blocked, sender_rejected, proxy_error, tarpit, no_signal.

What to do with each verdict

VerdictRecommended action
safeAccept. The mailbox exists and the domain is healthy.
riskyAccept with a lower expectation, or hold for a second look. reason says why (full inbox, busy mailbox, typo trap); is_full_inbox and spam_trap_risk carry the detail.
catchallMail with care, or keep as a separate segment. The domain accepts every address, so the mailbox itself is unconfirmed: reason distinguishes catch_all from accept_all, and the score orders how plausible the mailbox is inside the band.
invalidReject. If suggested_correction is set (for example user@gmail.com for user@gmial.com), offer it to the user.
unknownDo not treat as invalid. Retry later, or in a bulk job use Re-run unknowns once the mail server has settled. You were not charged.

Greylisting

Some mail servers refuse the first contact from an unfamiliar sender with a temporary error and accept a retry a few minutes later. That is greylisting, and it is the most common reason for an unknown result. EVA handles it differently depending on how you call it.

unknown never means invalid. Treat it as "ask again later", not as a reason to drop the address.

Evidence fields

FieldTypeMeaning
syntax_validbooleanThe address is well formed under RFC 5321 and 5322.
mx_foundbooleanThe domain publishes MX records. false together with smtp_check undeliverable means there is no mail server: no MX record, and nothing answers on port 25 at the address record that RFC 5321 falls back to, or DNS for the domain fails on two independent resolvers. The rare domain that runs mail on its bare address record still gets a real verdict.
is_disposablebooleanDomain is on the disposable and temporary-mail lists (110,000+ domains).
is_role_accountbooleanRole mailbox such as info@, admin@, support@.
is_catch_allbooleanDomain accepts mail for any local part, including Google Workspace tenants with catch-all routing, so an "accepted" answer from the mail server proves nothing about the mailbox. These addresses get the catchall verdict: the score orders them by how plausible the mailbox is, inside a band of 40 to 69, and never reads invalid unless the mailbox was actually refused. An account-level check with the provider can confirm the mailbox and lift the result to safe. Once the mailbox itself is answered, confirmed or refused, the address is no longer marked: is_catch_all and is_accept_all read false and reason carries neither catch_all nor accept_all. Accept-all consumer domains (Yahoo, Outlook.com, Mail.ru) are reported separately as is_accept_all; to catch both kinds, filter on reason (catch_all or accept_all) instead of this flag alone.
is_free_providerbooleanGmail, Outlook.com, Yahoo, and other consumer providers.
is_b2bbooleanCorporate domain rather than a consumer provider.
spam_trap_risklow · medium · highLikelihood the address is a spam trap or a recycled mailbox.
has_sub_addressingbooleanPlus-addressing in use (user+tag@). sub_address_base holds the base address.
suggested_correctionstring or nullLikely intended address when the domain looks like a typo.
smtp_providerstringDetected provider (for example google, microsoft, yahoo) and therefore which verification strategy ran.
deliverabilityobjectDomain health: grade A to F, score, authentication (SPF, DKIM, DMARC), and blacklist results across 10 zones. Only listings on zones receivers enforce affect the score; evaluated_checks shows how many zones returned a usable answer.
domain_ageobjectRegistration age in days and a risk level; very new domains score lower.
has_gravatarbooleanA Gravatar profile exists for the address, a weak signal that a person uses it.

Every field, including the provider-specific ones, is listed in the reference.

Rate limits and quotas

Two limits apply to every key: a per-minute request budget and a monthly quota of verifications. Both depend on your plan.

PlanRequests per minuteVerifications per monthBulk upload cap
Free10300300 per file, plus any credits held (up to 10,000)
Starter6020,00010,000 per file
Pro20050,00020,000 per file
Business500150,00050,000 per file
Enterprise1,000custom2,000,000 per file

Headers

HeaderMeaning
X-RateLimit-LimitYour per-minute budget (or the monthly quota on a quota error).
X-RateLimit-RemainingRequests left in the current window.
X-RateLimit-ResetUnix timestamp (seconds) when the window resets.
Retry-AfterOn a 429, seconds to wait before retrying.

Exceeding the per-minute budget returns 429 RATE_LIMITED with Retry-After: 60. Using up the monthly quota returns 429 QUOTA_EXCEEDED, with X-RateLimit-Reset set to the end of the billing window. Accounts on the Free plan without credits also have a limit of 50 checks a day: past it, single, batch and domain deliverability checks return 429 DAILY_LIMIT with X-Daily-Limit, X-Daily-Remaining (what is left today: 0, or more when a batch is larger than what is left) and Retry-After (seconds until 00:00 UTC). Successful checks on such an account carry X-Daily-Limit and X-Daily-Remaining. Any plan or credit pack removes the limit.

Errors

Errors use one shape: a human-readable message and a stable code you can branch on.

response · 429 Too Many Requests
{ "error": "rate limit exceeded", "code": "RATE_LIMITED" }
StatusCodeWhenWhat to do
400INVALID_INPUTMissing or malformed parameter, batch over 100 addresses, unsupported file type.Fix the request; do not retry as-is.
401UNAUTHORIZEDMissing, revoked, or unknown API key.Check the X-API-Key header.
403FORBIDDENAccount suspended, or the key may not perform this action.Contact support if unexpected.
404NOT_FOUNDUnknown job id or resource.Check the id.
409INVALID_STATEPause, resume, or cancel on a bulk job that is not in a state that allows it.Check the job's status first.
429RATE_LIMITEDPer-minute budget used up.Wait Retry-After seconds, then retry.
429QUOTA_EXCEEDEDMonthly quota used up.Add a credit pack or wait for the window to reset.
429DAILY_LIMITFree plan without credits: 50 checks a day used.Wait until 00:00 UTC (Retry-After), or add a plan or credit pack.
429BULK_LIMIT_EXCEEDEDThree bulk jobs are already running on the account.Wait for one to finish, then upload again.
429RERUN_COOLDOWNRe-run unknowns requested before the 30-minute greylist window has passed.Retry at rerun_available_at from the job.
500INTERNAL_ERRORSomething failed on our side.Retry with backoff; the address was not billed.
503SERVICE_UNAVAILABLEA dependency is temporarily down.Retry with backoff; check status.

Single verification

GET /api/v1/verify?email=… or POST /api/v1/verify with a JSON body {"email": "…"}. Both return the same result. Most addresses come back in under a second. A server that greylists first contact returns unknown within the 15-second deadline; see Greylisting.

# GET
curl "https://verifications.email/api/v1/verify?email=user@gmail.com" \
  -H "X-API-Key: $EVA_API_KEY"

# POST
curl -X POST https://verifications.email/api/v1/verify \
  -H "X-API-Key: $EVA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"email": "user@gmail.com"}'

Batch (up to 100)

POST /api/v1/verify/batch verifies up to 100 addresses in one synchronous call. Duplicates are removed before processing. Each address counts against your per-minute budget. For anything larger, use a bulk job.

curl -X POST https://verifications.email/api/v1/verify/batch \
  -H "X-API-Key: $EVA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"emails": ["user@gmail.com", "info@example.com", "nobody@mailinator.com"]}'
response · 200 OK
{
  "data": {
    "total": 3,
    "results": [
      { "email": "user@gmail.com", "score": 94, "risk": "safe", "smtp_check": "deliverable", … },
      { "email": "info@example.com", "score": 55, "risk": "risky", "is_role_account": true, … },
      { "email": "nobody@mailinator.com", "score": 45, "risk": "risky", "is_disposable": true, … }
    ]
  }
}

Bulk jobs

Upload a file, let the job run in the background, then collect the results by polling or through a webhook. Available on Starter and above; per-file caps are in the plan table.

File format

Upload, wait, download

# 1. upload (multipart). webhook_url is optional.
curl -X POST https://verifications.email/api/v1/verify/async/upload \
  -H "X-API-Key: $EVA_API_KEY" \
  -F "file=@subscribers.csv" \
  -F "webhook_url=https://example.com/hooks/eva"

# 2. poll until status is "completed"
curl https://verifications.email/api/v1/verify/async/JOB_ID \
  -H "X-API-Key: $EVA_API_KEY"

# 3. results as JSON, or as CSV with ?format=csv (add &risk=invalid to filter)
curl "https://verifications.email/api/v1/verify/async/JOB_ID/result?format=csv" \
  -H "X-API-Key: $EVA_API_KEY" -o results.csv
job object · 201 Created on upload, 200 OK on status
{
  "data": {
    "id": "5c1e0a7e-3b2f-4d61-9a0c-8f2d1b4e6a90",
    "status": "processing",
    "total_count": 12480,
    "completed_count": 9310,
    "webhook_url": "https://example.com/hooks/eva",
    "created_at": "2026-09-05T08:12:44Z",
    "started_at": "2026-09-05T08:12:46Z",
    "completed_at": null,
    "rerun_available_at": null
  }
}

Job lifecycle

StatusMeaning
pendingAccepted and queued.
processingRunning. completed_count advances as addresses resolve. Temporarily unknown addresses are re-probed while the job runs, up to three times, and once the count reaches the total the job waits up to five more minutes for retries already scheduled before completing. See Greylisting.
completedResults are ready. If a webhook URL was set, the event fires now.
pausedStopped by …/pause; resume with …/resume. Addresses already resolved keep their results.
cancelledStopped for good by …/cancel. Results resolved so far remain downloadable.
failedThe job could not finish; nothing unresolved was billed. Contact support with the job id.

POST …/async/{id}/pause, …/resume, and …/cancel control a running job; a control that does not fit the job's current state returns 409 INVALID_STATE. Up to three jobs run at once per account. GET /api/v1/verify/async lists your jobs.

Results

GET /api/v1/verify/async/{id}/result returns an array with one row per address: the verdict fields (score, risk, reason, smtp_check), the evidence flags (syntax_valid, mx_found, is_disposable, is_role_account, is_catch_all, is_free_provider, is_b2b, spam_trap_risk, is_full_inbox, is_locked, has_sub_addressing, suggested_correction, smtp_provider), and the domain summary (deliverability_grade, deliverability_score, domain_age_days, domain_age_risk). Add ?format=csv for a download and ?risk=safe|risky|catchall|invalid|unknown to filter either format.

Jobs from a free-only account (Free plan, no credits) show a summary and up to 10 sample rows until the account has any plan or credit pack. GET /api/v1/verify/async/{id}/summary returns job_id, total, the verdict counts (safe, risky, catchall, invalid, unknown), free_sample and results_locked, plus up to 10 result rows in sample while results are locked. While locked, /result (JSON and CSV) and rerun-unknowns answer 403 RESULTS_LOCKED. Job objects carry the same two flags. Buying a plan or pack unlocks the full results, including for earlier uploads.

Re-run unknowns

Unknowns are not billed and are worth a second pass once the mail servers that greylisted you have settled. POST /api/v1/verify/async/{id}/rerun-unknowns creates a follow-up job containing only that job's unknown addresses and returns it with 201 Created; poll the new job id and read its results the same way. The original job and its results are unchanged. A re-run becomes available 30 minutes after the job completes (rerun_available_at on the job); before that the API returns 429 RERUN_COOLDOWN with a Retry-After header.

Domain deliverability

GET /api/v1/domain/{domain}/deliverability grades a domain on its own: SPF, DKIM, and DMARC posture, presence on 10 blacklist zones, MX presence, and reputation. Useful for scoring a signup's company domain or auditing your own.

curl https://verifications.email/api/v1/domain/example.com/deliverability \
  -H "X-API-Key: $EVA_API_KEY"
response · 200 OK
{
  "data": {
    "domain": "example.com",
    "score": 88,
    "grade": "B",
    "authentication": {
      "spf": { "found": true, "policy": "strict" },
      "dkim": { "found": true },
      "dmarc": { "found": true, "policy": "quarantine" }
    },
    "blacklist": { "listed": false, "listed_count": 0, "high_confidence_count": 0, "total_checks": 10, "evaluated_checks": 10 },
    "has_mx": true,
    "reputation_score": 81,
    "checked_at": "2026-09-05T08:20:01Z"
  }
}

Webhooks

When a bulk job completes, EVA can call a URL of yours instead of you polling. Set webhook_url on the upload for a single job, or add endpoints under Webhooks in the dashboard to receive every job's event. The dashboard also shows a delivery log with each attempt's status code.

Event: bulk_job.completed

POST to your URL · Content-Type: application/json
{
  "event": "bulk_job.completed",
  "job_id": "5c1e0a7e-3b2f-4d61-9a0c-8f2d1b4e6a90",
  "status": "completed",
  "total_count": 12480,
  "completed_count": 12480,
  "summary": { "safe": 10112, "risky": 1089, "catchall": 314, "invalid": 871, "unknown": 94 },
  "result_url": "https://verifications.email/api/v1/verify/async/5c1e0a7e-3b2f-4d61-9a0c-8f2d1b4e6a90/result",
  "timestamp": "2026-09-05T08:41:07Z"
}

Verify the signature

Every delivery carries X-EVA-Signature: the hex HMAC-SHA256 of the raw request body, keyed with your signing secret from the dashboard's Webhooks tab. Compare it in constant time before trusting the payload. The libraries do this for you.

POST /hooks/eva HTTP/1.1
Content-Type: application/json
User-Agent: EVA-Webhook/1.0
X-EVA-Signature: 9f2c…e41a   # hex HMAC-SHA256(raw body, signing secret)

Delivery rules

Libraries

Official, MIT-licensed clients that wrap everything on this page, retry rate-limited requests with backoff, and verify webhook signatures.

JavaScript / TypeScript

Node 18+, Deno, Bun. ESM and CommonJS. Fully typed.

npm install @verifications-email/sdk

Python

Python 3.9+. Import name is eva_email. Typed models.

pip install verifications-email

Postman and other languages

A Postman collection with an environment file is on the integrations page. For any other language, generate a client from the OpenAPI 3 spec, or call the REST endpoints directly: everything is plain JSON over HTTPS with one header.

MCP server

AI agents can use verifications.email through a remote Model Context Protocol server. There is nothing to install: point your client at https://mcp.verifications.email/mcp and either sign in when the client asks, or pass your API key as a bearer token. Every tool call bills exactly like the REST API, and unknown results stay free.

Claude Code

Shell
claude mcp add --transport http email-verification https://mcp.verifications.email/mcp
# then type /mcp in a session to sign in. With an API key instead:
claude mcp add --transport http email-verification https://mcp.verifications.email/mcp \
  --header "Authorization: Bearer eva_your_key"

Cursor, Windsurf, VS Code

mcp.json
{
  "mcpServers": {
    "email-verification": {
      "url": "https://mcp.verifications.email/mcp",
      "headers": { "Authorization": "Bearer eva_your_key" }
    }
  }
}

n8n

Add an MCP Client Tool node, choose the HTTP Streamable transport, set the endpoint to https://mcp.verifications.email/mcp, and create a Bearer credential holding your API key. The node lists the tools below and any AI Agent node can call them.

Connect by signing in (no API key)

Clients that speak OAuth discover the sign-in flow from the server itself. Paste the URL, approve the connection in your browser, and the client keeps its own token. Connections appear under Connected apps on the API keys tab of the dashboard, where you can disconnect any of them. Every plan can connect, including Free.

Discovery documents live at https://mcp.verifications.email/.well-known/oauth-protected-resource/mcp and https://verifications.email/.well-known/oauth-authorization-server. Tokens are scoped to this server only; they never work against the REST API.

API key instead of sign-in

Automation that cannot open a browser (n8n, servers, CI) keeps using Authorization: Bearer eva_your_key, exactly as in the examples above.

Tools

ToolInputCost
verify_single_emailemail1 verification
verify_email_listemails (up to 100)1 per unique address
check_domain_deliverabilitydomain1 verification
create_bulk_verification_jobemails, webhook_urlbilled as the job completes
get_bulk_verification_job_statusjob_idfree
get_bulk_verification_job_resultsjob_id, risk, limit, offsetfree
list_bulk_verification_jobsnonefree
get_usage_summarynonefree

Quota and rate-limit problems come back as tool errors the agent can read, using the same codes as the REST API (QUOTA_EXCEEDED, RATE_LIMITED). Usage appears in the dashboard under the source mcp. If a call was paid with credits, credits for unknown results are returned.

Stuck, or a result looks wrong?

Write to support with the address or job id and what you expected. A human answers.