Email validation API reference: endpoints, fields, scores and errors
Everything the MailGate Email Check API returns, how the 0 to 100 score is worked out, and what each error means. Two endpoints, JSON in and out, CORS open, no signup for the free tier (20 checks a day per IP).
Base URL
https://pw-d20261007a2.plainwork-apps.workers.dev
GET /check
Checks one address. Pass email as a query parameter. Add your key as x-api-key header or ?key= for the paid tier. The x-tier response header says free or paid.
curl "https://pw-d20261007a2.plainwork-apps.workers.dev/[email protected]"
{
"email": "[email protected]",
"valid_syntax": true,
"local": "jane",
"domain": "gmial.com",
"disposable": false,
"free_provider": false,
"role_account": false,
"plus_tag": false,
"did_you_mean": "[email protected]",
"mx": "a-only",
"mx_hosts": [],
"deliverable_domain": true,
"score": 45,
"verdict": "risky"
}POST /bulk (paid key)
Send {"emails": [...]} with up to 100 addresses. Extra addresses beyond 100 are ignored. The response is {"count": n, "results": [...]} in request order, each result shaped like /check.
curl -X POST "https://pw-d20261007a2.plainwork-apps.workers.dev/bulk" \
-H "x-api-key: YOUR_KEY" -H "content-type: application/json" \
-d '{"emails":["[email protected]","[email protected]"]}'Fields
| Field | Meaning |
|---|---|
valid_syntax | Passes length limits (254 total) and allowed-character rules for the local part and domain. |
disposable | Domain is on the throwaway list. See the disposable domain list. |
role_account | Local part is a shared mailbox such as info or support; a +tag is ignored. See role-based addresses. |
free_provider | Gmail, Outlook and similar consumer providers. |
plus_tag | Local part contains +. |
did_you_mean | Suggested address when the domain is a known typo. See common domain typos. |
mx / mx_hosts | DNS result for the domain: mail hosts, or a-only when there is no MX but an A record exists. |
deliverable_domain | The domain has working MX (not a null MX) or falls back to an A record. |
score, verdict | See below. |
How the score works
A valid address starts at 100. If the domain cannot receive mail the score is 0, or 60 when the DNS lookup itself failed (so a flaky resolver does not reject real users). Then: disposable minus 70, role account minus 20, known typo minus 40, A-record-only minus 15. The floor is 0.
| Verdict | Score | Suggested action |
|---|---|---|
deliverable | 80 to 100 | Accept. |
risky | 40 to 79 | Accept with a prompt: show the typo suggestion, or ask to confirm a role address. |
undeliverable | 0 to 39 | Reject, or ask for another address (disposable, or domain that cannot receive mail). |
invalid | 0 | Bad syntax; reason is bad_syntax. |
Errors
| Status | error | Cause |
|---|---|---|
| 400 | missing ?email= or bad JSON | Required input absent. |
| 401 | invalid_or_inactive_key | The key is wrong or the subscription ended. |
| 402 | paid_key_required | /bulk called without a key. |
| 429 | free_limit_reached | 20 free checks used today from this IP. |
| 404 | not_found | Unknown path. |
Example in JavaScript
const r = await fetch("https://pw-d20261007a2.plainwork-apps.workers.dev/check?email=" + encodeURIComponent(email));
const c = await r.json();
if (c.verdict === "undeliverable" || c.verdict === "invalid") reject();
else if (c.did_you_mean) ask("Did you mean " + c.did_you_mean + "?");What it does not do
It checks the domain, never the mailbox, and it does not connect to mail servers, so it cannot prove an inbox exists. The disposable list is static (284 domains) and will miss new throwaway services. Calls are not logged beyond a per-IP daily counter for the free tier. Try it live on the free tester, or get unlimited checks and bulk for $5/month on the product page. A machine-readable OpenAPI 3 description ships in the open-source repo.