Validate email addresses in Express with middleware
In Express, add a middleware before your signup route that calls the SimpleVerifier API with the submitted email and responds 422 when the status is invalid. Pass everything else on with next(), keep the result on res.locals, and make the helper return null on timeouts so a slow check never turns into a 500.
By SimpleVerifier Team
Before you start
1. Add the helper
The same helper as the Node.js guide, with no dependencies. It never throws, which matters in Express 4, where a rejected promise in middleware isn't caught for you.
// simpleverifier.mjs — Node.js 18+, no dependencies
const API_URL = "https://simpleverifier.com/api/v1/verify";
const EMAIL_SHAPE = /^[^@\s]+@[^@\s]+\.[^@\s]+$/;
/**
* Resolves to "valid", "risky" or "invalid", or null when the API could not
* answer (timeout, rate limit, outage). Never throws.
*/
export async function emailStatus(email) {
const address = String(email ?? "").trim();
if (!EMAIL_SHAPE.test(address)) return "invalid"; // no API call made
try {
const res = await fetch(`${API_URL}?email=${encodeURIComponent(address)}`, {
headers: { Authorization: `Bearer ${process.env.SIMPLEVERIFIER_KEY}` },
signal: AbortSignal.timeout(30_000),
});
if (!res.ok) {
console.warn(`SimpleVerifier returned HTTP ${res.status}`);
return null; // 401, 402, 429, 503: fail open
}
const data = await res.json();
return data.status ?? null;
} catch (err) {
console.warn(`SimpleVerifier unreachable: ${err.name}`);
return null;
}
}2. Write the middleware
It reads req.body.email, so mount it after the body parsers.
// requireDeliverableEmail.mjs — uses emailStatus() from simpleverifier.mjs above
import { emailStatus } from "./simpleverifier.mjs";
export async function requireDeliverableEmail(req, res, next) {
const status = await emailStatus(req.body?.email);
if (status === "invalid") {
return res.status(422).json({ error: "This email address cannot receive mail. Please check it." });
}
res.locals.emailStatus = status; // "valid", "risky" or null (API unavailable)
next();
}3. Mount it on the route
Works for HTML form posts and JSON bodies.
// server.mjs
import express from "express";
import { requireDeliverableEmail } from "./requireDeliverableEmail.mjs";
const app = express();
app.use(express.urlencoded({ extended: false }));
app.use(express.json());
app.post("/signup", requireDeliverableEmail, (req, res) => {
// Save the signup here. res.locals.emailStatus tells you if it was risky.
res.status(201).json({ ok: true });
});
app.listen(3000);What the API returns
GET or POST https://simpleverifier.com/api/v1/verify with email runs the syntax, domain, disposable-domain and live mailbox checks in one call and returns:
{
"email": "jane@company.com",
"status": "valid",
"reason": "Mailbox exists",
"mailbox_checked": true
}Every snippet above follows the same rule: only a successful response with status set to invalid blocks a person.
| Result | What it means | What to do |
|---|---|---|
| valid | The mail server confirmed the mailbox exists. | Accept. |
| risky | The domain is real but the mailbox couldn't be confirmed: catch-all domains, role addresses like info@, providers that hide mailbox status. | Accept on signup forms. Store the status and keep these out of cold campaigns. |
| invalid | Bad format, no mail server, a disposable domain, or the mailbox doesn't exist. | Reject and ask the person to check the address. |
| timeout, 429 or 503 | The check didn't finish: a slow mail server, too many calls this minute, or a brief outage. | Accept (fail open) and log it. Re-check later if it matters. |
| 401 or 402 | The key is wrong or revoked, or the subscription isn't active. | Accept, and alert yourself: every check is being skipped until it's fixed. |
Errors return JSON with an error message. Each key can make 120 calls a minute; for thousands of addresses, upload a CSV in the dashboard instead.
Phone numbers and spreadsheets
The same key works with /api/v1/phone?phone=…&country=… to validate a phone number and normalise it to E.164, with live carrier lookups on the Email + Phone plan. The request and response are in the API reference. To check a column of addresses without code, use the Google Sheets add-on with the same key.
Common questions
- Can I use this with express-validator?
- Yes. Use body("email").isEmail() for the format, then .custom(async (value) => { if ((await emailStatus(value)) === "invalid") throw new Error("This email address cannot receive mail"); }) for the mailbox check. The custom validator only needs to throw for invalid.
- Should I check the address on every request?
- Only where an address is entered: signup, invitations, checkout, contact forms. Save the status with the record so you don't call the API again for the same address.
- How much does the SimpleVerifier API cost?
- API access is included in the paid plans: Pro at $29.99/month and Email + Phone at $49.99/month. There are no per-call credits. Each key can make 120 calls a minute.
- Should I reject risky addresses?
- Not on a signup or contact form. Risky means the mailbox couldn't be confirmed, which includes every address at a catch-all company domain. Accept it, store the status, and leave risky addresses out of cold outreach.
- What if the API is slow or down?
- Let the address through. A mailbox check can take several seconds when a mail server is slow, so cap the wait at about 30 seconds (less on a busy form) and treat a timeout, a 429 or a 503 as unknown rather than invalid.