MailProof
API reference

Three endpoints. JSON in, JSON out.

No key, no SDK, no ceremony. Every endpoint is a plain HTTP call and returns the same result object you see in the web console.

Verify a single address

POST /api/verify — the same endpoint also accepts GET with an ?email= query parameter, which is handy for a quick check from the browser.

Request
curl -X POST https://your-app.vercel.app/api/verify \
  -H "Content-Type: application/json" \
  -d '{"email": "ada@stripe.com"}'
Response · 200
{
  "email": "ada@stripe.com",
  "status": "unverified",
  "score": 70,
  "reason": "The domain accepts mail via Google Workspace",
  "limitation": "Google Workspace accepts every recipient at the SMTP layer, so whether this specific mailbox exists cannot be confirmed by any external tool.",
  "domain": "stripe.com",
  "mx": "aspmx.l.google.com",
  "provider": "Google Workspace",
  "free": false,
  "role": false,
  "disposable": false,
  "catchAllLikely": true,
  "didYouMean": null,
  "checks": {
    "syntax": true,
    "domain": true,
    "mx": true,
    "notDisposable": true,
    "notRole": true,
    "mailboxConfirmed": false
  }
}

Verify a batch

POST /api/verify/batch — up to 100 addresses per call, verified concurrently. For larger lists, send several batches in parallel; the web console uses three at a time, which is a sensible default.

Request
curl -X POST https://your-app.vercel.app/api/verify/batch \
  -H "Content-Type: application/json" \
  -d '{"emails": ["ada@stripe.com", "info@acme.io", "x@gmial.com"]}'
Response · 200
{
  "results": [ /* one result object per address, same shape as above */ ],
  "count": 3
}

A batch of more than 100 addresses returns 413 with an explanatory message rather than silently truncating.

Parse a file

POST /api/parse — send a CSV, TSV, text or Excel file as multipart/form-data under the field file. Every cell is scanned, addresses are lowercased and deduplicated, and the file is discarded as soon as the response is written.

Request
curl -X POST https://your-app.vercel.app/api/parse \
  -F "file=@contacts.xlsx"
Response · 200
{
  "emails": ["ada@stripe.com", "grace@company.io"],
  "count": 2,
  "truncated": false,
  "filename": "contacts.xlsx"
}

Health

GET /api/health — returns the service version and the list of checks currently enabled. Useful as an uptime probe.

The result object

FieldTypeMeaning
emailstringThe address as you submitted it.
statusstringOne of deliverable, unverified, risky, undeliverable, unknown.
scorenumberConfidence. Capped at 70 without a mailbox check; 100 only when SMTP confirms.
reasonstringPlain-language explanation of the status.
limitationstring | nullWhat was NOT established — e.g. mailbox not tested.
domainstringThe domain portion of the address.
mxstring | nullPrimary mail exchanger, by MX priority.
providerstring | nullMail platform behind the MX, when recognised.
freebooleanTrue for consumer mailbox providers such as Gmail.
rolebooleanTrue for shared inboxes such as info@ or support@.
disposablebooleanTrue for known throwaway providers.
catchAllLikelybooleanTrue when the server accepts every recipient (no mailbox is confirmable).
didYouMeanstring | nullSuggested domain when the one given looks like a typo.
checksobjectBooleans: syntax, domain, mx, notDisposable, notRole, mailboxConfirmed.

What the statuses mean

  • deliverable — the mail server confirmed this exact mailbox exists (SMTP), and the server is not accept-all. This is the only status that vouches for the mailbox itself, and it only appears where SMTP probing is enabled. Score 100.
  • unverified — the domain accepts mail, but the mailbox was not confirmed. Either SMTP was not run, or the server is accept-all (Gmail, Microsoft 365, any catch-all) where no mailbox can be confirmed. Worth sending to; not proven. Score capped at 70.
  • risky — the domain accepts mail, but the address is a shared role inbox or looks like a typo. Look before you send.
  • undeliverable — proven bad: invalid syntax, a dead domain, a disposable provider, or a mailbox the server explicitly rejected over SMTP. Remove it.
  • unknown — a check could not complete, usually a DNS or SMTP timeout. Retry rather than acting on it.

On the mailbox check (SMTP)

Confirming a specific mailbox — rather than just its domain — requires an SMTP RCPT TO probe over outbound port 25. That port is blocked on Vercel and most serverless platforms, so on the hosted site the probe does not run and the best status is unverified. Run this project anywhere port 25 is open (a VPS, or your own machine) with SMTP_PROBE=1 set, and the engine will additionally return deliverable for confirmed mailboxes and undeliverable for ones the server rejects.

Two limits hold even then, and neither is a bug. Google Workspace, Microsoft 365 and any catch-all domain accept every recipient at the SMTP layer and bounce privately later, so those never reach deliverable — we detect the accept-all behaviour and refuse to claim confirmation. And some servers greylist an unknown sender; that is reported as unknown, not as a rejection.

Rate limits are generous and unenforced during the open preview. If you plan to run sustained high volume, get in touch first so we can keep the service quick for everyone.

The Chrome extension & real mailbox checks

The browser extension checks the domain on its own. To confirm the mailbox — the part before the @ — it pairs with a tiny local helper, because an extension cannot open the SMTP connection a real check needs. Run the helper once from the project’s helper/ folder:

node mailproof-helper.js

Leave that window open. The extension detects it automatically, flips its badge to Real check, and starts returning deliverable and undeliverable for individual mailboxes. Close it and the extension falls back to domain-only checks. Nothing leaves your machine except the DNS and SMTP lookups the check itself requires; no address is stored.