ContactLintAPIMCPPricingFree toolsGet a key

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

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

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.