DNSai Email Verify DNSai Email Verify

Developers: API & MCP

Verify email addresses from your own code with the REST API, or from AI assistants such as Claude Code, Claude.ai, Claude Desktop and Codex through our MCP server.

Included with the Pro, Business and Enterprise plans. API usage draws on the same monthly credit balance as your dashboard, 1 credit per unique email. Create and manage your keys on the API & MCP page of your dashboard. Free, Starter and Growth plans can upgrade to unlock it.

On this page

  1. What it does
  2. Get an API key
  3. REST quickstart
  4. The result object
  5. Credits and result reuse
  6. Latency, load and delivery options
  7. Limits by plan
  8. Errors, retries, idempotency
  9. Completion email and webhooks
  10. Sandbox keys
  11. MCP: Claude Code, Claude.ai, Codex
  12. Python
  13. Reference

1. What it does

For each address the service runs syntax checks, DNS/MX lookups, a careful SMTP conversation with the receiving server (no email is ever sent), catch-all detection, and checks for disposable, role (sales@, info@) and free-mail addresses and likely typos (gmial.com → gmail.com). You get back one verdict (status), a precise reason (sub_status), a 0-100 score and a plain safe_to_send recommendation of yes, caution or no.

REST base URLhttps://verify-api.dnsai.com
MCP endpointhttps://verify-api.dnsai.com/mcp (Streamable HTTP, stateless)
AuthAuthorization: Bearer <key> on every request, REST and MCP alike
OpenAPIverify-api.dnsai.com/v1/openapi.json

2. Get an API key

1

Be on an API plan

Pro, Business or Enterprise. See pricing; existing accounts can upgrade from the dashboard.

2

Open API & MCP in your dashboard

Log in and click API in the header, or go to verify.dnsai.com/secure/api-access/.

3

Create a key

Name it after the device or integration (laptop, crm-sync, claude-code) and choose Live or Sandbox. Copy the key from the one-time dialog; we store only a hash and cannot show it again.

PrefixModeWhat it does
dv_live_xxxxxxxx_...liveReal verification. Costs credits.
dv_test_xxxxxxxx_...sandboxInstant, deterministic fake results. No probing, no credits. For development (§9).

Treat a key like a password. The first part, dv_live_xxxxxxxx, is a non-secret prefix that appears in logs and in GET /v1/account. Up to 10 active keys per account; revoke any of them from the same page. Revoking a key also disconnects any Claude.ai or Claude Desktop connector built on it.

export DNSAI_VERIFY_KEY=dv_live_xxxxxxxx_...

3. REST quickstart

API=https://verify-api.dnsai.com
AUTH="Authorization: Bearer $DNSAI_VERIFY_KEY"

curl -s "$API/v1/account" -H "$AUTH"          # plan, credits, limits, key info

Verify one address

curl -s -X POST "$API/v1/verify" -H "$AUTH" -H "Content-Type: application/json" \
     -H "Idempotency-Key: $(uuidgen)" \
     -d '{"email": "[email protected]", "ref": "crm-123"}'

Returns 200 with {"result": {...}} when the check finishes within wait seconds (default 25, max 55), otherwise 202 with a job_id to poll.

Batch job (JSON)

curl -s -X POST "$API/v1/jobs" -H "$AUTH" -H "Content-Type: application/json" \
     -H "Idempotency-Key: $(uuidgen)" \
     -d '{"items": [{"email": "[email protected]", "ref": "1"}, {"email": "[email protected]", "ref": "2"}],
          "label": "october-leads", "webhook_url": "https://example.com/hooks/dnsai"}'
# → 202 {"job_id": "job_...", "accepted": 2, "duplicates_removed": 0, "invalid": [], "credits_charged": 2, ...}

Batch job (CSV or TXT upload)

curl -s -X POST "$API/v1/jobs" -H "$AUTH" \
     -F "[email protected]" -F "email_column=Email" -F "label=leads"
# other CSV columns ride along as "meta" and come back with each result

Status, results, cancel

curl -s "$API/v1/jobs/$JOB?wait=55" -H "$AUTH"                       # long-poll until done
curl -s "$API/v1/jobs/$JOB/results?format=json&limit=1000" -H "$AUTH"  # pages; follow next_cursor
curl -s "$API/v1/jobs/$JOB/results?format=csv" -H "$AUTH" -o out.csv    # everything at once
curl -s "$API/v1/jobs/$JOB/results?status=deliverable" -H "$AUTH"       # filter by status
curl -s "$API/v1/jobs?status=running" -H "$AUTH"                         # list
curl -s -X DELETE "$API/v1/jobs/$JOB" -H "$AUTH"                         # cancel; unprocessed items refunded
curl -s "$API/v1/[email protected]" -H "$AUTH"                  # your latest result, free
curl -s "$API/v1/events?after=0" -H "$AUTH"                              # pull feed of job.completed events

4. The result object

{
  "email": "[email protected]", "ref": "crm-123",
  "status": "deliverable", "sub_status": "mailbox_confirmed",
  "score": 95, "safe_to_send": "yes",
  "flags": {"catch_all": false, "role": false, "free": false, "disposable": false, "gibberish": false},
  "did_you_mean": null,
  "domain": {"name": "acme.com", "mx_provider": "google", "health_score": 90},
  "reason": "Mailbox verified (SMTP 250)", "attempts": 1, "cached": false,
  "checked_at": "2026-10-13T16:01:13Z"
}
statussub_statusMeaning
deliverablemailbox_confirmedThe server confirmed the mailbox. Safe to send.
undeliverablemailbox_absent, domain_absent, syntaxWill bounce. Remove it.
riskycatch_all, accept_all_provider, mailbox_full, disposableAccepted but unprovable, or likely to bounce later. Use caution.
unknownprovider_blocked, temp_failure, timeout, no_signal, deadlineNo verdict could be reached. Retry later or treat as risky.

score is a deliverability band (deliverable 90-99, catch-all 50-69, unknown 30, mailbox absent 5, other undeliverable 0). safe_to_send is yes, caution or no.

5. Credits and result reuse

5b. Latency, load and choosing how results come back

Verification talks SMTP to the receiving mail server, and probing is paced per provider to keep the service trusted. So a fresh check takes seconds, not milliseconds, and when many customers submit at once, requests queue. Every response tells you where you stand:

SignalWhereMeaning
result.cached: trueany resultServed from a stored result (≤ max_age_days old): instant.
202 + estimated_seconds, queue.ahead, queue.load, Retry-AfterPOST /v1/verifyNot finished within wait; here is how long it will probably take.
eta_seconds, loadGET /v1/jobs/{id} while runningRough time to completion for that job.
GET /v1/status (MCP get_service_status)any time, freeCurrent load, queue depth, seconds to expect per single check and per 1,000 list items, and advice.
429 + Retry-AfteranyA plan limit was hit; back off for that long.

load is normal (single checks usually finish inside the request), busy (expect 202s; poll or use webhooks) or saturated (send lists as jobs with webhook_url or notify_email, and don't poll tightly).

A one-word decision on every result

"decision": {"action": "send",   "why": "mailbox confirmed"}
"decision": {"action": "review", "why": "domain accepts every address; mailbox unprovable"}
"decision": {"action": "review", "why": "mail server asked us to retry later", "retry_after_seconds": 1800}
"decision": {"action": "drop",   "why": "mailbox does not exist"}

send = deliverable. drop = undeliverable, disposable or mailbox full. review = catch-all or accept-all (unprovable) or unknown (no verdict yet; retry_after_seconds says when a fresh check is worth it). Shape the output to your needs: fields=status,decision,score trims any result; decision=send or decision=drop,review filters a job's results; format=csv|ndjson streams whole jobs, and the CSV has decision and decision_why columns.

Pick the delivery that fits the caller

You are…Do this
A form or CRM hook checking one address as the user types or savesPOST /v1/verify with wait 10-25. Treat 202 as "pending": show the user a check is in progress, then poll status_url?wait=25 (the result is in that response), or pass webhook_url with wait=0 and never poll. Up to 200 single checks may be in flight per account; they never count against your concurrent-jobs limit.
A lead-verification app sending a steady streamBatch what arrives over a few seconds into one POST /v1/jobs, give it webhook_url, and read rows from the webhook's results_url with format=ndjson or decision=send. Use Idempotency-Key so retries never charge twice.
A server syncing a databaseDrain GET /v1/events?after=<cursor>: every finished job appears once; single verifies carry data.result inline, lists carry counts and results_url.
A person cleaning a listPOST /v1/jobs with notify_email: a summary and CSV link arrive by email when it finishes. Or ask Claude through MCP and it polls for you.

Tell your users the truth: cached results are instant, most fresh checks take 3-15 seconds, greylisted or slow providers can take minutes, and a few addresses in any large list are retried for up to a couple of hours or come back unknown. A max_wall_seconds deadline on a job turns leftovers into unknown/deadline (refunded) so a nightly process always finishes.

6. Limits by plan

PlanRequests / minEmails / dayEmails per jobConcurrent jobs
Pro605,0005,0002
Business12010,00010,0003
Enterprise24020,00025,0005

Requests per minute apply per account (all keys together). Daily caps count live emails submitted; sandbox usage is free and uncounted. Your exact limits are in GET /v1/account → limits.

Other limitsValue
Server-side wait on POST /v1/verify0-55 s (default 25)
Job long-poll ?wait=at most 55 s
Results page sizeat most 1000 (REST); MCP get_job_results at most 200
MCP inline list100 addresses per call (use CSV upload or verify_list_from_url for more)
Upload size50 MB per file
Idempotency window24 h
Result and job retentionabout 30 days. Download what you want to keep

Throughput. Probing is paced per receiving provider to protect deliverability and reputation. Most of a batch finishes within minutes; addresses that hit temporary failures are retried over a few hours. Pass max_wall_seconds to put a deadline on a job (leftovers come back as unknown/deadline and are refunded). Use a webhook, the completion email, or ?wait= rather than tight polling.

7. Errors, retries and idempotency

Errors use application/problem+json with a machine-readable code and a human detail.

HTTPCodeWhat to do
400 / 422invalid_request, …Fix the request; don't retry.
401missing_api_key, invalid_api_keyCheck the key; it may be revoked.
402insufficient_creditsUpgrade or wait for the period reset.
403plan_not_entitled (body carries upgrade_url), account_inactiveUpgrade the plan.
413job_too_large, file_too_largeSplit the list.
429rate_limited, daily_cap_reached, too_many_active_jobsWait Retry-After seconds, then retry.
502 / 503 / 504Temporary; retry with backoff.

Idempotency. Send an Idempotency-Key header (a fresh UUID per logical request, reused unchanged on retries) on every POST. A replay returns the original response and never charges twice. Keys are remembered for 24 hours.

Retries. Retry 429, 502, 503, 504 and network errors (a dropped connection, "server disconnected", timeouts) with exponential backoff, honouring Retry-After. The API sits behind Cloudflare, which closes idle keep-alive connections from time to time; a client that reuses a pooled connection can see one dropped request. With the idempotency key the retry is free and safe. The Python client library does all of this for you.

8. Completion email and webhooks

When you create a job you can pass notify_email (a summary and CSV link, sent to your account's own address) and/or webhook_url (an HTTPS endpoint that receives a signed job.completed POST). Webhooks are signed with your account's webhook secret, shown on the API & MCP page:

X-DNSai-Signature: t=<unix time>,v1=<hex HMAC-SHA256(secret, t + "." + raw body)>

Deliveries are retried at 1 min, 5 min, 30 min, 2 h and 6 h. The job's status shows delivery details. Servers without a public endpoint can drain GET /v1/events instead.

9. Sandbox keys

Sandbox keys (dv_test_) exercise the whole API, including jobs, cursors, CSV, MCP and events, with deterministic fake results. Nothing is probed and no credits are used. The result depends on the local part:

Local part containsstatus / sub_status
invalidundeliverable / mailbox_absent
catchallrisky / catch_all
unknownunknown
fullrisky / mailbox_full
disposablerisky / disposable
anything elsedeliverable

10. MCP: use it from AI assistants

The MCP server at https://verify-api.dnsai.com/mcp offers tools such as verify_email, verify_emails, verify_list_from_url, get_job, get_job_results, list_jobs, lookup_email, cancel_job and get_account, plus prompts for cleaning a prospect list. Every tool returns within about 25 seconds; longer work returns a job_id the assistant polls.

Claude Code

claude mcp add --transport http dnsai-verify https://verify-api.dnsai.com/mcp \
  --header "Authorization: Bearer $DNSAI_VERIFY_KEY"
# add --scope user to make it available in every project
# then: "verify these emails: [email protected], [email protected]"  or  "clean leads.csv before the campaign"

Claude.ai and Claude Desktop

  1. Create a key on the API & MCP page (for example named claude-ai).
  2. Open Settings → Connectors → Add custom connector, enter https://verify-api.dnsai.com/mcp, leave client ID and secret empty.
  3. Click Connect. A DNSai consent page opens; paste the key and approve. The connection acts as that key, and revoking the key disconnects it.

OpenAI Codex CLI

# ~/.codex/config.toml
[mcp_servers.dnsai_verify]
url = "https://verify-api.dnsai.com/mcp"
bearer_token_env_var = "DNSAI_VERIFY_KEY"
tool_timeout_sec = 60

Claude Messages API (MCP connector)

client.beta.messages.create(
    model="claude-sonnet-5-5", max_tokens=2000,
    betas=["mcp-client-2025-11-20"],
    mcp_servers=[{"type": "url", "url": "https://verify-api.dnsai.com/mcp",
                  "name": "dnsai-verify", "authorization_token": DNSAI_VERIFY_KEY}],
    tools=[{"type": "mcp_toolset", "mcp_server_name": "dnsai-verify"}],
    messages=[{"role": "user", "content": "Verify [email protected] and tell me if it is safe to send."}])

Any other client that supports Streamable HTTP with custom headers works the same way: the URL plus the Authorization: Bearer header. The server is stateless, so no session setup is needed.

11. Python

import os, requests, uuid
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry

API = "https://verify-api.dnsai.com"
H = {"Authorization": "Bearer " + os.environ["DNSAI_VERIFY_KEY"]}
s = requests.Session()   # retry dropped connections and 429/5xx, honouring Retry-After
s.mount("https://", HTTPAdapter(max_retries=Retry(total=5, backoff_factor=1, status_forcelist=[429, 502, 503, 504],
                                                  allowed_methods=["GET", "POST", "DELETE"], respect_retry_after_header=True)))

r = s.post(f"{API}/v1/verify", headers={**H, "Idempotency-Key": str(uuid.uuid4())},
           json={"email": "[email protected]"}, timeout=60).json()
print(r["result"]["status"], r["result"]["decision"]["action"])

job = s.post(f"{API}/v1/jobs", headers={**H, "Idempotency-Key": str(uuid.uuid4())},
             json={"emails": ["[email protected]", "[email protected]"], "label": "demo"}).json()
while True:
    st = s.get(f"{API}/v1/jobs/{job['job_id']}", headers=H, params={"wait": 55}, timeout=90).json()
    if st["status"] in ("completed", "failed", "canceled"):
        break
    print("still running:", st["done"], "of", st["total"], "eta", st.get("eta_seconds"), "s")
rows = s.get(f"{API}/v1/jobs/{job['job_id']}/results", headers=H,
             params={"format": "json", "limit": 1000}).json()["results"]

12. Reference

Method & pathPurpose
POST /v1/verifyOne address. {email, ref?, max_age_days?, force_fresh?, wait?}
POST /v1/jobsBatch. JSON {emails[] | items[{email, ref, meta}], label?, max_age_days?, force_fresh?, notify_email?, webhook_url?, max_wall_seconds?} or multipart file (+ email_column)
GET /v1/jobsList jobs (status, limit, cursor)
GET /v1/jobs/{id}Status and counts; ?wait= long-polls (≤55 s)
GET /v1/jobs/{id}/resultsformat=json|ndjson|csv, status=, cursor, limit (≤1000)
DELETE /v1/jobs/{id}Cancel; unprocessed items refunded
GET /v1/lookup?email=Your latest result for an address (free)
GET /v1/events?after=Pull feed of job.completed events
GET /v1/accountPlan, credits, limits, key info, webhook secret
GET /v1/statusCurrent load, queue depth and expected waits
GET /v1/healthLiveness (no auth)
GET /v1/openapi.jsonOpenAPI description (no auth)

Ready to build?

Create your first key on the API & MCP page of your dashboard. Questions: [email protected].

Open API & MCP