Home / Validate Email Addresses on Form Submit (Node, Python, PHP)
Validate Email Addresses on Form Submit (Node, Python, PHP)
Published 2026-10-17
Browser checks are easy to skip, so the place to reject a bad address for good is your server, right before you save it. This guide shows one request to the ContactLint API from Node, Python and PHP, and a simple rule for what to do with each answer.
The request
Send the address to POST https://contactlint.com/v1/email/validate with your secret key as Authorization: Bearer YOUR_KEY (or an x-api-key header). Get a free key at /signup. The response looks like this (some fields left out; the full list is in the OpenAPI spec):
{"result": {"input": "jane@gmial.com", "syntax_valid": true, "domain": "gmial.com",
"disposable": false, "role_address": false, "did_you_mean": "jane@gmail.com",
"reason": "possible_typo", "verdict": "risky", ...},
"usage": {"billed_as": "free", "free_used_this_month": 1, "free_limit": 100, "credits_remaining": 0}}
The verdict is one of valid, risky, invalid or unknown.
A simple rule for each verdict
- invalid: the address is malformed or its domain has no MX or A record, so mail cannot be delivered. Reject it and ask for another address.
- risky with
did_you_mean: show "Did you mean ...?" and let the person confirm or fix it. - risky because
disposableis true: reject it if you need a permanent address, otherwise accept it. - risky because
role_addressis true (info@, sales@ and similar): usually accept it. It is a real inbox, just a shared one. - valid or unknown: accept it. An unknown caused by a failed DNS lookup is not billed.
Whatever you choose, fail open: if the API is slow or unreachable, save the address anyway. A short timeout keeps a validation problem from ever blocking a signup.
Node (18 or later, built-in fetch)
async function checkEmail(email) {
try {
const res = await fetch('https://contactlint.com/v1/email/validate', {
method: 'POST',
headers: { 'Authorization': 'Bearer ' + process.env.CONTACTLINT_KEY, 'content-type': 'application/json' },
body: JSON.stringify({ email }),
signal: AbortSignal.timeout(3000),
});
if (!res.ok) return { ok: true }; // quota used up or other error: fail open
const { result } = await res.json();
if (result.verdict === 'invalid') return { ok: false, message: 'That email address cannot receive mail.' };
if (result.did_you_mean) return { ok: false, suggestion: result.did_you_mean };
if (result.disposable) return { ok: false, message: 'Please use a permanent email address.' };
return { ok: true };
} catch {
return { ok: true }; // timeout or network error: fail open
}
}
Python (requests)
import os, requests
def check_email(email):
try:
r = requests.post('https://contactlint.com/v1/email/validate',
headers={'Authorization': 'Bearer ' + os.environ['CONTACTLINT_KEY']},
json={'email': email}, timeout=3)
if r.status_code != 200:
return {'ok': True} # fail open
result = r.json()['result']
except requests.RequestException:
return {'ok': True} # fail open
if result['verdict'] == 'invalid':
return {'ok': False, 'message': 'That email address cannot receive mail.'}
if result['did_you_mean']:
return {'ok': False, 'suggestion': result['did_you_mean']}
if result['disposable']:
return {'ok': False, 'message': 'Please use a permanent email address.'}
return {'ok': True}
PHP (curl)
function check_email($email) {
$ch = curl_init('https://contactlint.com/v1/email/validate');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 3,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('CONTACTLINT_KEY'), 'Content-Type: application/json'],
CURLOPT_POSTFIELDS => json_encode(['email' => $email]),
]);
$body = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($body === false || $code !== 200) return ['ok' => true]; // fail open
$r = json_decode($body, true)['result'];
if ($r['verdict'] === 'invalid') return ['ok' => false, 'message' => 'That email address cannot receive mail.'];
if (!empty($r['did_you_mean'])) return ['ok' => false, 'suggestion' => $r['did_you_mean']];
if ($r['disposable']) return ['ok' => false, 'message' => 'Please use a permanent email address.'];
return ['ok' => true];
}
Errors to expect
- 401
missing_api_keyor an invalid key: check the header. - 400
bad_request: the body has noemailstring. - 402
quota_exceeded: the 100 free checks for the month are used and no credits remain. Buy a pack at /buy or keep failing open until next month.
What this does not check
ContactLint does not connect to mail servers, so it cannot tell you whether a specific mailbox exists or whether a domain accepts all addresses (catch-all). It checks syntax, MX and A records, known disposable domains, role addresses and typos of major providers. If you need mailbox-level checks, see the comparison with mailbox verifiers.
Want the hint in the browser too?
The server check is the one that counts. For a friendlier form, add the drop-in widget so people see "Did you mean ...?" before they press submit, and keep the server check as the backstop.
Get a free API key. 100 checks per month, no card required.