Email Check API Reference
POST/v1/email/validate
Checks one email address: syntax, known disposable/temporary domains, the domain's mail server (MX), likely typos and role addresses, and returns an accept/review/reject verdict with the reasons behind it. Send a JSON body of { "email": "user@example.com" }. Designed to be called from a signup form's own JavaScript, using the same publishable, origin-locked key model as GET /v1/geo. For many addresses or a server-side signup decision, use the batch, list-job and signup-check endpoints further down this page. All checks are domain- and address-level: no mailbox is contacted, so no result establishes that a mailbox exists, who owns it, or that mail to it will arrive.
Authentication
POST /v1/email/validate takes an origin-locked publishable key via the X-Api-Key header — the same key type and allowed-origins model as GET /v1/geo. Every other endpoint on this page (batch, list jobs, signup check) accepts many addresses or an arbitrary IP, so it requires your secret key via Authorization: Bearer and must only be called from your backend; a publishable key gets a 403 there.
Response fields
- The address you sent, echoed back exactly as submitted.
- domain
- The lowercased domain (internationalized domains in punycode), or null when syntaxValid is false.
- syntaxValid
- Whether the address is syntactically valid. Not a mailbox-existence or deliverability check.
- isDisposable
- Whether the domain matches a known disposable/temporary email provider — directly, through a parent domain, or through a mail server known to serve disposable domains. Always false when syntaxValid is false.
- normalizedEmail
- Trimmed, lowercased address with the domain in punycode, or null when syntaxValid is false.
- canonicalEmail
- normalizedEmail with any +tag removed (and for Gmail, dots removed and googlemail.com mapped to gmail.com) — store it to spot the same person signing up twice. Null when syntaxValid is false.
- disposableSource
- Why isDisposable is true: list (the domain itself is listed), parent_domain (a parent domain is listed, e.g. a.b.mailinator.com), or mx_host (the domain's mail server serves known disposable domains). Null when isDisposable is false.
- isFreeProvider
- Whether the domain is a consumer mailbox provider such as gmail.com or outlook.com. Informational only — it never changes the verdict.
- isRoleAccount
- Whether the local part is a role address such as admin@, info@ or noreply@.
- didYouMean
- A likely typo correction of the whole address (john@gmial.com → john@gmail.com), or null. A domain with its own working mail server is not “corrected” unless it is one edit away from a common provider.
- mx.status
- found (MX records exist), implicit (no MX, but an A/AAAA record), none (neither), null_mx (the domain declares it accepts no mail), no_domain (the domain does not exist), or unknown (the lookup failed or timed out). The whole mx object is null when syntaxValid is false.
- mx.host
- The lowest-preference MX host when status is found, otherwise null. Says nothing about whether the individual mailbox exists.
- verdict
- accept, review or reject — the most severe of the conditions in reasons. A recommendation for your own policy, not a final decision.
- reasons
- Every condition that was hit, most severe first; empty when verdict is accept. See “Verdict and reason codes” below.
Errors
- 400
- Malformed request body. Batch: empty_batch or batch_too_large (more than 100 addresses). Signup check: invalid_ip. List jobs: invalid_body, empty_job or job_too_large (more than 100,000 addresses). An invalid email address is never a 400 — it is a result with verdict reject and reason invalid_syntax.
- 401
- Missing or invalid API key.
- 403
- POST /v1/email/validate: the request's Origin/Referer is not on the key's allowed-origins list (no CORS header is set, so the browser blocks the response regardless of the JSON body). Secret-key endpoints: a publishable key was used.
- 404
- job_not_found — no list job with this id for your account.
- 409
- job_not_completed — the job is still queued or processing, or it failed; results are only downloadable once it has completed.
- 410
- job_expired — the job's uploaded addresses and results have been deleted (7 days after creation).
- 413
- body_too_large — a CSV job upload larger than 10 MB.
- 415
- A list-job body that is neither JSON nor text/csv.
- 429
- The per-minute rate limit, or monthly_limit_exceeded when the plan's monthly request cap would be exceeded — see errors and rate limits for how to tell which. A batch or list job that would exceed the cap is rejected whole, before any work is done.
- 503
- job_storage_unavailable — a list job could not be stored. Nothing was charged; retry.
Example
Request
curl https://api.watch-ip.com/v1/email/validate \
-H "X-Api-Key: YOUR_PUBLISHABLE_KEY" \
-H "Content-Type: application/json" \
-d '{"email": "John.Doe+news@Gmial.com"}'Response
{
"email": "John.Doe+news@Gmial.com",
"domain": "gmial.com",
"syntaxValid": true,
"isDisposable": false,
"normalizedEmail": "john.doe+news@gmial.com",
"canonicalEmail": "john.doe@gmial.com",
"disposableSource": null,
"isFreeProvider": false,
"isRoleAccount": false,
"didYouMean": "john.doe+news@gmail.com",
"mx": {
"status": "none",
"host": null
},
"verdict": "reject",
"reasons": [
"no_mail_server",
"typo_suspected"
]
}Verdict and reason codes
Each condition sets a minimum verdict; the most severe one wins, and reasons lists all of them, most severe first. Whether a free-provider address is acceptable is left to you — it never moves the verdict.
- invalid_syntax
- Reject. The address is not syntactically valid (length limits, allowed characters, dot placement, a real domain rather than an IP literal). Quoted local parts are rejected too — legal but effectively never used at signup.
- domain_not_found
- Reject. The domain does not exist in DNS.
- no_mail_server
- Reject. The domain declares that it accepts no mail (a “null MX”), or has neither MX nor A/AAAA records.
- disposable_domain
- Reject. The domain, a parent domain, or the domain's mail server matches known disposable/temporary email providers — see disposableSource.
- typo_suspected
- Review. didYouMean holds a likely correction, such as gmial.com → gmail.com.
- role_account
- Review. The local part is a role address such as admin@, info@, support@ or noreply@, usually shared rather than personal.
- implicit_mx
- Review. The domain has no MX record; mail would fall back to its A/AAAA address, which is rare for real mail domains.
- dns_unavailable
- Review. The DNS lookup failed or timed out, so the domain's mail setup is unknown (mx.status: unknown). Not evidence against the address.
Batch check
POST/v1/email/batch
Checks 1–100 addresses in one request with the same checks and result shape as POST /v1/email/validate, one result per address in input order. Send { "emails": [...] }. Addresses on the same domain share one DNS lookup. Counts one request per address against your monthly plan cap; a batch that would exceed the cap is rejected whole with 429 monthly_limit_exceeded. Toward the per-minute rate limit, the whole call counts as one request. Secret key only.
Request
curl https://api.watch-ip.com/v1/email/batch \
-H "Authorization: Bearer YOUR_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{"emails": ["user@gmail.com", "someone@mailinator.com"]}'Response
{
"results": [
{ "email": "user@gmail.com", "verdict": "accept", "reasons": [], ... },
{ "email": "someone@mailinator.com", "isDisposable": true, "disposableSource": "list", "verdict": "reject", "reasons": ["disposable_domain"], ... }
]
}Email list jobs
POST/v1/email/jobs
Checks up to 100,000 addresses in the background. Send JSON { "emails": [...], "filename": "..." } or a text/csv body of at most 10 MB: the column headed email (also e-mail, email address, email_address or mail, case-insensitive) is used, or the first column when no header names one; comma, semicolon and tab separators are detected. The job is charged when it is created — one request per distinct address (exact match), so duplicates are checked but charged once — and a list that would exceed your monthly cap is rejected whole. If part of a job cannot be processed after retries, the job is marked failed and the credits for the unprocessed part are returned. Answers 202 with the job object. Secret key only; the same jobs can also be uploaded and downloaded from the dashboard's Email lists page.
Request
curl https://api.watch-ip.com/v1/email/jobs \
-H "Authorization: Bearer YOUR_SECRET_KEY" \
-H "Content-Type: text/csv" \
--data-binary @newsletter.csvResponse
{
"id": "5b0d6f5e-8a51-4f7e-9d0e-2c1f3f7f8a11",
"status": "queued",
"source": "api",
"filename": null,
"total": 12840,
"credits": 12512,
"processed": 0,
"summary": null,
"createdAt": "2026-09-28T10:00:00.000Z",
"completedAt": null,
"expiresAt": "2026-10-05T10:00:00.000Z"
}Job status
GET/v1/email/jobs/{id}
Returns the same job object. Poll it until status is completed; polling is not charged.
- status
- queued → processing → completed. failed: part of the list could not be processed after retries; no results are available and credits for the unprocessed part were returned. expired: the uploaded addresses and results were deleted 7 days after creation.
- total
- Rows submitted, duplicates included.
- credits
- Requests charged: one per distinct address.
- processed
- Rows checked so far.
- summary
- Once completed: row counts per verdict (accept/review/reject) and per reason code that occurred. Null before that.
- expiresAt
- When the uploaded addresses and results are deleted.
Job results
GET/v1/email/jobs/{id}/results
Downloads a CSV with one row per submitted row, in submission order — duplicates included. Columns: email, normalized_email, verdict, reasons, syntax_valid, is_disposable, disposable_source, is_free_provider, is_role_account, did_you_mean, mx_status, mx_host. reasons is semicolon-separated, empty cells mean null, and mx_status is empty for syntactically invalid addresses (nothing was looked up). An invalid address that starts with =, +, - or @ is prefixed with an apostrophe so spreadsheet apps don't run it as a formula. 409 until the job has completed, 410 once it has expired. Downloading is not charged.
Request
curl https://api.watch-ip.com/v1/email/jobs/5b0d6f5e-8a51-4f7e-9d0e-2c1f3f7f8a11/results \
-H "Authorization: Bearer YOUR_SECRET_KEY" -o results.csvSignup check
POST/v1/signup/check
Scores a signup from its email address and IP in one call, from your backend when the signup form is submitted (a verdict computed in the browser can be forged). Send { "email": "...", "ip": "...", "userAgent": "..." } — userAgent is optional. The response contains the full email check result (email), the IP's country, ASN and — on plans that include security data — its VPN/Tor/datacenter/threat indicators (ip), the email domain's age in days from public registration (RDAP) data (emailDomainAgeDays, null when unknown and for well-known consumer providers), a 0–100 score (the sum of the reason weights below, capped at 100) and a verdict: reject at 60 or more, review at 25 or more, otherwise accept. On a plan without security data, ip.security is null, the score uses the email signals only, and reasons includes ip_security_unavailable. Counts as one request. Secret key only.
- invalid_syntax
- 70
- domain_not_found · no_mail_server · disposable_domain
- 60 each
- ip_tor · ip_threat
- 40 each
- ip_vpn
- 25
- new_domain
- 25 — the email domain was registered less than 30 days ago
- ip_datacenter
- 20 — only when the IP is not already a VPN or Tor exit
- automated_user_agent
- 20 — the supplied userAgent looks like a bot or script
- typo_suspected
- 15
- role_account · implicit_mx
- 10 each
- dns_unavailable
- 5
- ip_security_unavailable
- 0 — informational: this plan has no security data
Request
curl https://api.watch-ip.com/v1/signup/check \
-H "Authorization: Bearer YOUR_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{"email": "jane@new-startup.io", "ip": "203.0.113.1"}'Response
{
"email": { "email": "jane@new-startup.io", "verdict": "accept", "reasons": [], ... },
"ip": {
"ip": "203.0.113.1",
"country": "US",
"asn": 64500,
"asOrganization": "Example Hosting",
"security": { "isTor": false, "isVpn": false, "isDatacenter": true, "isThreat": false, "isSanctionedNetwork": false }
},
"emailDomainAgeDays": 12,
"score": 45,
"verdict": "review",
"reasons": ["new_domain", "ip_datacenter"]
}Limits
Every address checked counts as one request against your plan's shared monthly cap — a single check, each address in a batch, each distinct address in a list job — and a signup check is one request. Batches and list jobs are admitted or rejected whole, never partially fulfilled. The per-minute rate limit counts HTTP calls, not items: a batch of up to 100 IPs or email addresses is one request toward it, while each item still counts separately toward the monthly request cap. List-job uploads and results are deleted 7 days after the job is created. DNS lookups have a short time budget: when one fails or times out the result degrades to mx.status unknown and reason dns_unavailable rather than an error. Disposable-domain data is merged from three community-maintained open-source lists, refreshed every few hours and extended by mail servers observed to serve listed domains — see data sources. A very new disposable provider may not be flagged yet. All checks are domain- and address-level: no mailbox is contacted, so no result establishes that a mailbox exists, who owns it, or that mail to it will arrive.
Related pages
- AuthenticationPublishable vs. secret keys, origin-locking, and rate limits.
- GET /v1/geo referenceFull field reference for the visitor geolocation endpoint.
- Disposable email detectionFlag disposable, mistyped and no-mail-server signup emails, one at a time or in bulk.
- Signup abuse preventionCombine email and IP signals into a signup accept, review or reject decision.
- Data sourcesWhich source backs each response field, and its known gaps.