Developers · API & MCP

Verify from your code. Or your agent.

A REST API and a remote MCP server for ZenVerifier. Same credits, same results and the same 30-day rules as the dashboard, with keys scoped to exactly what they need.

REST + JSONMCP over HTTP120 req/min per key
POST /api/v1/verify
$ curl -X POST https://www.zenverifier.com/api/v1/verify \
    -H "Authorization: Bearer $ZV_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"email":"jane@example.com"}'

{
  "data": {
    "email": "jane@example.com",
    "result": "ok",
    "cached": false,
    "charged": 1,
    "creditsBalance": 24179
  }
}
Start here

Quickstart

Base URL https://www.zenverifier.com/api/v1. JSON in, JSON out, HTTPS only.

  1. Create a key. In the dashboard, open API & MCP in the sidebar and create a key. Owners and admins can do this. The Server integration — verify + lists preset covers everything below. The key is shown once, so put it straight into your secret store.
  2. Verify an address.
    bash
    export ZV_API_KEY="zv_live_YOUR_KEY"
    
    curl -X POST https://www.zenverifier.com/api/v1/verify \
      -H "Authorization: Bearer $ZV_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{"email":"jane@example.com"}'
  3. Read the answer. Act on result: keep ok, decide on catch_all, drop invalid and disposable, try unknown again later.
    200 OK · application/json
    {
      "data": {
        "id": 48213,
        "email": "jane@example.com",
        "result": "ok",
        "quality": "good",
        "subresult": "ok",
        "free": false,
        "role": false,
        "didYouMean": null,
        "cached": false,
        "charged": 1,
        "creditsBalance": 24179,
        "verifiedAt": "2026-10-12T09:41:07.000Z"
      }
    }
Keys

Authentication

Every request carries an API key in the Authorization header:

header
Authorization: Bearer zv_live_YOUR_KEY
  • A key belongs to one workspace and sees only that workspace’s lists, history and credits.
  • Keys start with zv_live_ and are shown once, when created. We store only a SHA-256 hash, so a lost key can’t be recovered: roll it or create a new one.
  • Send keys only in the Authorization header, never in a URL or query string. The API has no CORS support on purpose: call it from your servers, not from a browser.
  • If the person who created a key leaves the workspace, the key stops working.
  • Owners and admins get an email whenever a key is created, rolled or revoked. The email shows the key’s prefix (zv_live_ab12), never the key.

Scopes

A key can only do what its scopes allow. read:* covers every …:read scope and * covers everything. Write scopes don’t include the matching read scope.

ScopeAllows
account:readRead workspace plan and credit balance
verify:writeVerify single emails (spends credits)
verifications:readRead single-verification history
lists:readRead bulk lists, status and results
lists:writeCreate bulk lists (spends credits)
mcp:accessConnect AI agents over MCP (tools still limited by the other scopes)
read:*Read everything
*Full access

Presets

The dashboard creates keys from these presets. The first one is the default. The last column shows which MCP tools a key made from the preset can use.

PresetScopesMCP tools
Server integration — verify + listsaccount:readverify:writeverifications:readlists:readlists:writeNo MCP access
AI agent (MCP) — verify + listsmcp:accessaccount:readverify:writeverifications:readlists:readlists:writeget_accountverify_emailverify_emailslist_verificationscreate_listlist_listsget_listget_list_results
AI agent (MCP) — read-onlymcp:accessread:*get_accountlist_verificationslist_listsget_listget_list_results
Single verify onlyaccount:readverify:writeNo MCP access
Read-onlyread:*No MCP access
Full access (everything, incl. MCP)*get_accountverify_emailverify_emailslist_verificationscreate_listlist_listsget_listget_list_results

Expiry, rolling and revoking

  • Keys expire never, or after 30, 90 or 365 days. Owners and admins get an email 7 days before a key expires.
  • Roll issues a new key with the same name, scopes and expiry. The old key keeps working for 24 more hours and then stops, so you can swap keys without downtime.
  • Revoke stops a key immediately, including a rolled key that’s still in its 24-hour grace period.
Billing

Credits & caching

The API and MCP spend the same credits as the dashboard, under the same rules.

Single checks
POST /api/v1/verify · MCP verify_email, verify_emails
  • Checking the same address again in the same workspace within 30 days is free: you get the stored result back with charged: 0.
  • Otherwise it costs 1 credit, including when we answer from our shared cache of recent results (cached: true, charged: 1).
  • The credit is reserved before the check runs. If the check can’t complete (engine_unavailable), it’s refunded.
  • If a check would cost a credit and your balance is empty, you get 402 insufficient_credits and nothing runs.
Bulk lists
POST /api/v1/lists · MCP create_list
  • 1 credit per unique email. Duplicates are removed first and aren’t charged.
  • Credits are reserved when the list is submitted. If your balance can’t cover the whole list, you get 402 and no list is created.
  • If the list can’t start, the reservation is refunded.
  • Emails answered from the shared cache still count. The free 30-day re-check applies to single checks only.

Check your balance any time with GET /api/v1/account.

Reference

Endpoints

Single objects come back as { "data": … }. Collections are paginated by page number and add a pagination object. Every response carries Cache-Control: no-store. Responses to authenticated requests also carry the rate-limit headers (401 and 403 don’t).

MethodPathScope
GET/api/v1/accountaccount:read
POST/api/v1/verifyverify:writespends credits
GET/api/v1/verificationsverifications:read
GET/api/v1/listslists:read
POST/api/v1/listslists:writespends credits
GET/api/v1/lists/:idlists:read
GET/api/v1/lists/:id/resultslists:read
GET/api/v1/lists/:id/downloadlists:read
paginated response
{
  "data": [
    "…"
  ],
  "pagination": {
    "page": 2,
    "pageSize": 25,
    "totalPages": 7,
    "total": 161
  }
}

GET/api/v1/account

scope · account:read

The workspace this key belongs to, its plan and its current credit balance.

request · bash
curl https://www.zenverifier.com/api/v1/account \
  -H "Authorization: Bearer $ZV_API_KEY"

planSource says where the plan comes from: direct (paid to ZenVerifier, or the free plan), outboundpass (included with an OutboundPass membership) or outboundpass_revoked (the membership ended and the workspace moved to the free plan).

POST/api/v1/verify

scope · verify:writespends credits

Verify one email address and wait for the answer. Costs 1 credit, or nothing if this workspace checked the same address in the last 30 days.

Body fieldTypeDescription
emailstringRequired. The address to check. Trimmed and lower-cased first.
request · bash
curl -X POST https://www.zenverifier.com/api/v1/verify \
  -H "Authorization: Bearer $ZV_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"email":"jane@example.com"}'

GET/api/v1/verifications

scope · verifications:read

Single-email verification history for the workspace, newest first. Includes checks made from the dashboard, the API and MCP.

Query parameterTypeDescription
pageintegerPage number, starting at 1. Default 1.
pageSizeinteger1–100. Default 25.
emailstringOptional. Only checks of this address.
request · bash
curl "https://www.zenverifier.com/api/v1/verifications?email=jane@example.com" \
  -H "Authorization: Bearer $ZV_API_KEY"

GET/api/v1/lists

scope · lists:read

Bulk lists in the workspace, newest first, with status and result counts.

Query parameterTypeDescription
pageintegerPage number, starting at 1. Default 1.
pageSizeinteger1–100. Default 25.
statusstringOptional. One of queued, processing, finished, failed, canceled, paused.
request · bash
curl "https://www.zenverifier.com/api/v1/lists?status=finished" \
  -H "Authorization: Bearer $ZV_API_KEY"

POST/api/v1/lists

scope · lists:writespends creditsIdempotency-Key

Start a bulk verification. Send JSON with an array of emails, or upload a file as multipart form data and we pick out every email address in it. Addresses are trimmed, lower-cased and de-duplicated, up to 50,000 unique emails per list. 1 credit per unique email is reserved when the list is submitted. Returns 201 straight away; the list runs in the background.

Body fieldTypeDescription
emailsstring[]JSON body. Required unless you send a file.
filefileMultipart form data. A CSV or plain-text file. Required unless you send emails.
namestringOptional label shown in the dashboard.
HeaderDescription
Idempotency-KeyOptional, 1–255 characters. Makes retries safe. See Idempotency.
request · bash
curl -X POST https://www.zenverifier.com/api/v1/lists \
  -H "Authorization: Bearer $ZV_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: october-signups-2026-10-12" \
  -d '{"name":"October signups","emails":["jane@example.com","sales@example.org"]}'

# or upload a file
curl -X POST https://www.zenverifier.com/api/v1/lists \
  -H "Authorization: Bearer $ZV_API_KEY" \
  -F "file=@signups.csv" \
  -F "name=October signups"

GET/api/v1/lists/:id

scope · lists:read

Status, progress and result counts for one list. While a list is running, each read fetches fresh progress at most every 15 seconds, so poll every 15–30 seconds.

request · bash
curl https://www.zenverifier.com/api/v1/lists/1207 \
  -H "Authorization: Bearer $ZV_API_KEY"

GET/api/v1/lists/:id/results

scope · lists:read

Per-email results of a finished list as JSON, paginated. Returns 409 list_not_finished until the list’s status is finished.

Query parameterTypeDescription
resultstringOptional. Comma-separated: ok, catch_all, invalid, disposable, unknown. Default: every result.
pageintegerPage number, starting at 1. Default 1.
pageSizeinteger1–1000. Default 100.
request · bash
curl "https://www.zenverifier.com/api/v1/lists/1207/results?result=ok,catch_all&pageSize=500" \
  -H "Authorization: Bearer $ZV_API_KEY"

partial is true when part of the results couldn’t be loaded on this request. Retry the request rather than treating those rows as the complete list. If it stays true across retries, that part is no longer available. Contact support.

GET/api/v1/lists/:id/download

scope · lists:read

The results of a finished list as a CSV attachment, the same files the dashboard offers. Columns: email,result,quality,subresult,free,role,didyoumean.

Query parameterTypeDescription
filterstringok (deliverable only) · ok_and_catch_all (default) · unknown · invalid · all (every row).
request · bash
curl -o october-clean.csv \
  "https://www.zenverifier.com/api/v1/lists/1207/download?filter=ok_and_catch_all" \
  -H "Authorization: Bearer $ZV_API_KEY"

Verification fields

FieldTypeDescription
idintegerVerification id.
emailstringThe normalised (trimmed, lower-case) address.
resultstringok · catch_all · invalid · disposable · unknown. See Result values.
qualitystring | nullCoarse quality rating for the address.
subresultstring | nullFiner-grained reason behind result.
freebooleanFree webmail provider (personal inbox).
rolebooleanRole address that reaches a team rather than a person (info@, sales@).
didYouMeanstring | nullSuggested correction for a likely typo.
cachedbooleanAnswered from a recent result instead of a fresh check.
charged0 | 1Credits this request spent. Verify responses only.
creditsBalanceintegerWorkspace balance after this request. Verify responses only.
verifiedAtISO 8601When the result was produced.

List fields

FieldTypeDescription
idintegerList id.
namestringLabel shown in the dashboard.
statusstringqueued · processing · finished · failed · canceled · paused.
progressinteger0–100.
createdViastringdashboard · api · mcp.
emailsintegerUnique emails in the list.
resultsobjectCounts: ok, catchAll, invalid, disposable, unknown.
cachedintegerEmails answered from recent results instead of fresh checks.
creditsChargedintegerCredits charged for this list.
errorstring | nullWhy the list failed, when status is failed.
createdAtISO 8601When the list was submitted.
finishedAtISO 8601 | nullWhen it finished.
expiresAtISO 8601When the list and its results are deleted (createdAt + 30 days).
Reference

Result values

Every verification and every list row has one result. List counts use the same five buckets (catch_all is catchAll there).

resultMeaning
okDeliverable. The mailbox exists and accepts mail.
catch_allThe domain accepts mail for any address, so this mailbox can’t be confirmed. Send with care.
invalidThe address doesn’t exist or can’t receive mail. Remove it.
disposableA throwaway or temporary inbox provider.
unknownNo definitive answer this time, usually a slow or greylisting mail server.
Guide

Bulk lists

Lists run in the background. Up to 50,000 unique emails per list over the API (5,000 over MCP); split anything bigger.

  1. Submit with POST /api/v1/lists, as JSON or a file upload. Send an Idempotency-Key so a retry can’t create (and charge for) the list twice. You get the list back with status: "queued".
  2. Poll GET /api/v1/lists/:id every 15–30 seconds until status is finished (or failed, with the reason in error). progress goes from 0 to 100.
  3. Collect the rows as JSON from /results (filter by result, paginate) or as a CSV from /download.

Lists made over the API or MCP show up in the dashboard too, and createdVia tells you where each one came from. Results are deleted 30 days after the list was created, so collect them before expiresAt.

Reference

Errors

Errors use standard HTTP status codes and one JSON shape: { "error": code, "message": text, "field"?: name }, sometimes with extra fields (for example balance). Branch on error. message is written for people and may change.

HTTP 403
{
  "error": "forbidden",
  "message": "Missing scope: lists:write"
}
HTTPerrorWhen
400invalid_jsonThe request body isn’t valid JSON.
400validation_errorA parameter failed validation. field names the parameter.
401unauthorizedMissing or malformed Authorization header, or the key is invalid, expired or revoked.
402insufficient_creditsNot enough credits. The body adds balance and needed (bulk lists also missing). Nothing was charged.
403forbiddenThe key doesn’t have the scope this endpoint needs. The message names it.
404not_foundNo such list or verification in this key’s workspace.
409list_not_finishedThe list is still running. Poll GET /api/v1/lists/:id until status is finished.
409no_resultsThe list finished but there are no results to return.
409idempotency_in_progressA request with this Idempotency-Key is still running. Retry shortly.
413payload_too_largeMore than 50,000 unique emails in one list. Split it.
422idempotency_key_reusedThis Idempotency-Key was already used with a different request body.
429rate_limitedOver the per-minute limit for this key. Wait for Retry-After.
500internal_errorSomething unexpected went wrong on our side. Safe to retry.
502engine_unavailableThe verification engine couldn’t be reached. Any credits reserved for the request are refunded. Retry shortly.
Limits

Rate limits

Each key gets 120 requests per minute by default, counted in fixed one-minute windows. MCP tool calls count against the same budget, one request per call, except verify_emails, which counts one per distinct address. Over the limit, the API answers 429 rate_limited and MCP tools return a rate_limited error. Wait Retry-After seconds, then carry on.

HeaderMeaning
X-RateLimit-LimitRequests allowed per minute for this key.
X-RateLimit-RemainingRequests left in the current minute.
X-RateLimit-ResetWhen the window resets, as a Unix timestamp in seconds.
Retry-AfterOnly on 429: seconds to wait before retrying.
response headers
HTTP/1.1 200 OK
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 117
X-RateLimit-Reset: 1791798360
Cache-Control: no-store
Retries

Idempotency

POST /api/v1/lists spends credits, so retries need care. Send an Idempotency-Key header: 1–255 characters, one per list you mean to create (a UUID works).

  • Same key and same body after the first request finished: you get the original response again, with Idempotent-Replayed: true. No second list, no second charge.
  • Same key while the first request is still running: 409 idempotency_in_progress.
  • Same key with a different body: 422 idempotency_key_reused.
  • If the first request failed with an error, the key is released and you can retry with it.
  • Keys are remembered for 24 hours.
  • Over MCP, create_list takes the same thing as its idempotency_key argument.

Single checks don’t need a key: re-checking an address within 30 days is free.

AI agents

MCP server

ZenVerifier runs a remote Model Context Protocol server, so Claude, Cursor and other MCP clients can check addresses and run lists for you, using the same API keys, credits and rate limits.

Endpoint
https://www.zenverifier.com/api/mcp
Transport
Streamable HTTP, stateless, JSON responses. POST only.
Auth
Authorization: Bearer zv_live_… with the mcp:access scope. Use an “AI agent (MCP)” preset.

The quickest route: in the dashboard, API & MCP → Generate key & copy config creates a key and copies your client’s config with the key filled in. To set it up by hand, replace zv_live_YOUR_KEY below with your key.

terminal
claude mcp add --transport http zenverifier https://www.zenverifier.com/api/mcp --header "Authorization: Bearer zv_live_YOUR_KEY"

Tools

mcp:access lets a key connect; its other scopes decide which tools it sees. A read-only agent key can look up lists and history but can’t spend credits. Tool errors come back as tool results with isError: true and the same codes as the REST API.

ToolScopeArgumentsWhat it does
get_accountaccount:readnoneGet account. Workspace name, plan, where the plan comes from (planSource) and current credit balance.
verify_emailspends creditsverify:writeemailVerify one email. Verify a single email address. Costs 1 credit unless this workspace checked the same address in the last 30 days (then free).
verify_emailsspends creditsverify:writeemails (1–10)Verify up to 10 emails. Verify up to 10 email addresses in one call. Same pricing as verify_email, per address; each distinct address also counts as one request against the rate limit. For more, use create_list.
list_verificationsverifications:reademail?, page?, page_size? (≤100)Recent single checks. Recent single-email verification results, newest first. Optional email filter.
create_listspends creditslists:writeemails (1–5,000), name?, idempotency_key?Create bulk list. Start a bulk verification for up to 5,000 emails. Reserves 1 credit per unique email up front. Runs asynchronously: poll get_list until status is finished, then read get_list_results.
list_listslists:readstatus?, page?, page_size?List bulk lists. Bulk lists in this workspace, newest first, with status and result counts.
get_listlists:readlist_idGet list status. Status, progress and result counts for one bulk list.
get_list_resultslists:readlist_id, result? (array), page?, page_size? (≤500)Get list results. Per-email results of a finished list, filterable by result (ok, catch_all, invalid, disposable, unknown), paginated.
  • The first time a key makes an MCP call, owners and admins get an email. Every call is logged (tool, client, status, duration) and the latest ones show under API & MCP in the dashboard. Call logs are kept for 90 days.
  • Give each client its own key, so you can revoke one without breaking the others. Only paste keys into clients you trust.
Privacy

Data retention

  • Bulk lists and their results are deleted 30 days after the list was created. Every list carries expiresAt; after that it returns 404 not_found.
  • API keys are stored only as hashes. The full key is never logged and never emailed.
  • See Privacy, DPA and Security for the rest.

Ship it with 50 free.

No credit card · Same credits in the dashboard, API and MCP
API & MCP · ZenVerifier