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"
import { EvaClient } from '@verifications-email/sdk';
const eva = new EvaClient({ apiKey: process.env.EVA_API_KEY });
const { data } = await eva.verify('user@gmail.com');
console.log(data.risk, data.score); // "safe" 94
import os
from eva_email import EvaClient
eva = EvaClient(api_key=os.environ["EVA_API_KEY"])
result = eva.verify("user@gmail.com")
print(result.data.risk, result.data.score) # safe 94
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.
{
"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.
X-API-Key: eva_3f9c2a1b8e7d4c6f0a9b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f
- Keep keys on the server. A key in browser or mobile code can be read by anyone who loads the page.
- A missing or unknown key returns
401 UNAUTHORIZED. A key on a suspended account, or one that may not perform the action, returns403 FORBIDDEN. - Dashboard sessions use a separate JWT; you never need it for verification.
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
| Field | Values | Meaning |
|---|---|---|
risk | safe · 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. |
score | 0 to 100 | Composite 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_check | deliverable · 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. |
reason | code, 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
| Verdict | Recommended action |
|---|---|
safe | Accept. The mailbox exists and the domain is healthy. |
risky | Accept 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. |
catchall | Mail 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. |
invalid | Reject. If suggested_correction is set (for example user@gmail.com for user@gmial.com), offer it to the user. |
unknown | Do 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.
- Single and batch verification run under a 15-second request
deadline, so a greylisting server comes back as
unknowninstead of holding your request. The result is not billed, the domain is remembered as a greylister, and a retry a few minutes later usually resolves it. - Bulk jobs absorb the wait for you. Addresses that came back
unknownfor a temporary reason are re-probed while the job is still running, up to three times: a greylisting server that names its window is retried after that window, otherwise after one, two and four minutes; a rate-limited exit is retried from a different outbound IP after a minute. Once the last address of the list has been processed, the job waits up to five more minutes for retries already scheduled, then finishes; anything still waiting at that point, or in the middle of a retry, staysunknownfor Re-run unknowns. Addresses whose cause cannot change within minutes, such as a mail server that blocks our IP outright or a domain whose mail host is down, are not retried inside the job. - Re-run unknowns covers whatever is still unresolved after the job completes. It unlocks 30 minutes later, when greylist windows have typically cleared, and creates a follow-up job with only those addresses.
unknown never means invalid. Treat it as "ask again later", not as a reason to drop the address.
Evidence fields
| Field | Type | Meaning |
|---|---|---|
syntax_valid | boolean | The address is well formed under RFC 5321 and 5322. |
mx_found | boolean | The 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_disposable | boolean | Domain is on the disposable and temporary-mail lists (110,000+ domains). |
is_role_account | boolean | Role mailbox such as info@, admin@, support@. |
is_catch_all | boolean | Domain 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_provider | boolean | Gmail, Outlook.com, Yahoo, and other consumer providers. |
is_b2b | boolean | Corporate domain rather than a consumer provider. |
spam_trap_risk | low · medium · high | Likelihood the address is a spam trap or a recycled mailbox. |
has_sub_addressing | boolean | Plus-addressing in use (user+tag@). sub_address_base holds the base address. |
suggested_correction | string or null | Likely intended address when the domain looks like a typo. |
smtp_provider | string | Detected provider (for example google, microsoft, yahoo) and therefore which verification strategy ran. |
deliverability | object | Domain 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_age | object | Registration age in days and a risk level; very new domains score lower. |
has_gravatar | boolean | A 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.
| Plan | Requests per minute | Verifications per month | Bulk upload cap |
|---|---|---|---|
| Free | 10 | 300 | 300 per file, plus any credits held (up to 10,000) |
| Starter | 60 | 20,000 | 10,000 per file |
| Pro | 200 | 50,000 | 20,000 per file |
| Business | 500 | 150,000 | 50,000 per file |
| Enterprise | 1,000 | custom | 2,000,000 per file |
- A batch request counts every address in its body against the per-minute budget, not just the one call.
- Unknown results are not counted against the monthly quota. When a call was paid with credits, the credits for unknown results are returned. Pay-as-you-go credit packs extend it at a flat $1.20 per 1,000 and never expire.
- The official libraries retry rate-limited requests automatically with backoff.
Headers
| Header | Meaning |
|---|---|
X-RateLimit-Limit | Your per-minute budget (or the monthly quota on a quota error). |
X-RateLimit-Remaining | Requests left in the current window. |
X-RateLimit-Reset | Unix timestamp (seconds) when the window resets. |
Retry-After | On 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.
{ "error": "rate limit exceeded", "code": "RATE_LIMITED" }
| Status | Code | When | What to do |
|---|---|---|---|
| 400 | INVALID_INPUT | Missing or malformed parameter, batch over 100 addresses, unsupported file type. | Fix the request; do not retry as-is. |
| 401 | UNAUTHORIZED | Missing, revoked, or unknown API key. | Check the X-API-Key header. |
| 403 | FORBIDDEN | Account suspended, or the key may not perform this action. | Contact support if unexpected. |
| 404 | NOT_FOUND | Unknown job id or resource. | Check the id. |
| 409 | INVALID_STATE | Pause, resume, or cancel on a bulk job that is not in a state that allows it. | Check the job's status first. |
| 429 | RATE_LIMITED | Per-minute budget used up. | Wait Retry-After seconds, then retry. |
| 429 | QUOTA_EXCEEDED | Monthly quota used up. | Add a credit pack or wait for the window to reset. |
| 429 | DAILY_LIMIT | Free plan without credits: 50 checks a day used. | Wait until 00:00 UTC (Retry-After), or add a plan or credit pack. |
| 429 | BULK_LIMIT_EXCEEDED | Three bulk jobs are already running on the account. | Wait for one to finish, then upload again. |
| 429 | RERUN_COOLDOWN | Re-run unknowns requested before the 30-minute greylist window has passed. | Retry at rerun_available_at from the job. |
| 500 | INTERNAL_ERROR | Something failed on our side. | Retry with backoff; the address was not billed. |
| 503 | SERVICE_UNAVAILABLE | A 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"}'
const { data, rateLimit } = await eva.verify('user@gmail.com');
if (data.risk === 'invalid') {
reject(data.suggested_correction); // "user@gmail.com" for a typo like gmial.com
} else if (data.risk === 'unknown') {
retryLater(); // not billed; the server gave no definitive answer
}
console.log(rateLimit.remaining); // requests left this minute
result = eva.verify("user@gmail.com")
data = result.data
if data.risk == "invalid":
reject(data.suggested_correction) # e.g. "user@gmail.com" for a typo like gmial.com
elif data.risk == "unknown":
retry_later() # not billed; the server gave no definitive answer
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"]}'
const { data } = await eva.verifyBatch([
'user@gmail.com', 'info@example.com', 'nobody@mailinator.com',
]);
for (const r of data.results) {
console.log(r.email, r.risk, r.score);
}
batch = eva.verify_batch([
"user@gmail.com", "info@example.com", "nobody@mailinator.com",
])
for r in batch.data.results:
print(r.email, r.risk, r.score)
{
"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
.csvor.xlsx, up to 11 MB, one address per row.- If the header row has a column named
email,email_address, ore-mail, that column is used. Otherwise the first column is. - Rows that do not contain a valid address are skipped, and duplicates are removed.
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
import { readFile } from 'node:fs/promises';
const file = await readFile('subscribers.csv');
const { data: job } = await eva.uploadBulk(file, {
filename: 'subscribers.csv',
webhookUrl: 'https://example.com/hooks/eva', // optional
});
const { data: done } = await eva.waitForBulkJob(job.id, {
onProgress: j => console.log(`${j.completed_count}/${j.total_count}`),
});
const { data: results } = await eva.getBulkJobResults(done.id);
const invalid = results.filter(r => r.risk === 'invalid');
with open("subscribers.csv", "rb") as f:
job = eva.upload_bulk(f, filename="subscribers.csv",
webhook_url="https://example.com/hooks/eva") # webhook_url optional
done = eva.wait_for_bulk_job(job.data.id) # polls until completed or failed
results = eva.get_bulk_job_results(done.data.id)
invalid = [r for r in results.data if r.risk == "invalid"]
{
"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
| Status | Meaning |
|---|---|
pending | Accepted and queued. |
processing | Running. 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. |
completed | Results are ready. If a webhook URL was set, the event fires now. |
paused | Stopped by …/pause; resume with …/resume. Addresses already resolved keep their results. |
cancelled | Stopped for good by …/cancel. Results resolved so far remain downloadable. |
failed | The 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"
const { data } = await eva.getDomainDeliverability('example.com');
console.log(data.grade, data.score, data.blacklist.listed);
report = eva.get_domain_deliverability("example.com")
print(report.data.grade, report.data.score, report.data.blacklist.listed)
{
"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
{
"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)
import { parseWebhookEvent } from '@verifications-email/sdk';
// Express: mount with express.raw({ type: 'application/json' }) so the body is untouched
app.post('/hooks/eva', async (req, res) => {
const event = await parseWebhookEvent({
payload: req.body, // raw Buffer or string
signature: req.get('X-EVA-Signature'),
secret: process.env.EVA_WEBHOOK_SECRET,
}); // throws if the signature is wrong
console.log(event.job_id, event.summary);
res.sendStatus(200);
});
from eva_email import parse_webhook_event
# Flask
@app.post("/hooks/eva")
def eva_hook():
event = parse_webhook_event(
request.get_data(), # raw bytes, not parsed JSON
request.headers["X-EVA-Signature"],
os.environ["EVA_WEBHOOK_SECRET"],
) # raises if the signature is wrong
print(event.job_id, event.summary)
return "", 200
Delivery rules
- The URL must be public HTTPS. Private and internal addresses are rejected at registration.
- Respond with any 2xx within 10 seconds. Do the heavy work after responding.
- Three attempts per event: immediately, then after 1 second, then after 5 seconds. After that the
delivery is marked failed in the dashboard log and the results stay available at
result_url. - Handle duplicates: use
job_idas an idempotency key.
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
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
{
"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.
- claude.ai, Claude Desktop, Cowork: Customize, Connectors, Add custom connector, paste
https://mcp.verifications.email/mcp, leave the client id blank, then Connect. - ChatGPT: Settings, Apps and connectors, enable developer mode, create a connector with the same URL and OAuth authentication.
- Claude Code:
claude mcp add --transport http email-verification https://mcp.verifications.email/mcp, then/mcpinside a session to sign in. - Cursor, Windsurf, VS Code: add the server with only its
url; the editor registers itself and opens the sign-in page.
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
| Tool | Input | Cost |
|---|---|---|
verify_single_email | 1 verification | |
verify_email_list | emails (up to 100) | 1 per unique address |
check_domain_deliverability | domain | 1 verification |
create_bulk_verification_job | emails, webhook_url | billed as the job completes |
get_bulk_verification_job_status | job_id | free |
get_bulk_verification_job_results | job_id, risk, limit, offset | free |
list_bulk_verification_jobs | none | free |
get_usage_summary | none | free |
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.