Home / Integrations / WordPress
Validate Emails in WordPress Forms With ContactLint
Published 2026-10-18
This guide adds the ContactLint widget to your WordPress site, so people see "Did you mean ...?" before they submit. It also adds a server-side check in the core registration form and in Contact Form 7. The server check stops bad addresses even when someone skips the browser hint.
Where the check goes in WordPress
WordPress gives you two places to check an address. The ContactLint widget runs in the browser and shows Did you mean jane@gmail.com? under the email field. A PHP check runs on your server when the form is submitted, right before WordPress saves the user or sends the message.
Use both, but trust the server. Anyone can turn off JavaScript or post to your form directly, so the browser hint is only a convenience. The server check cannot be skipped, and the server is the only place your secret key may live.
This guide uses two well known filters. registration_errors covers the core WordPress registration form. wpcf7_validate_email* and wpcf7_validate_email cover Contact Form 7. For other form plugins, use the widget plus the contactlint:result event shown in step 1, or follow that plugin's documentation for its own validation hook.
Step 1: add the widget
- Get a free secret key at /signup.
- Create a publishable key restricted to your site's origins. A key accepts up to 5 origins, so include staging if you have it.
curl -X POST https://contactlint.com/v1/widget/keys \
-H "Authorization: Bearer YOUR_SECRET_KEY" \
-H "content-type: application/json" \
-d '{"origins":["https://yoursite.com","https://staging.yoursite.com"],"daily_cap":200}'The response contains a publishable_key that starts with cl_pub_. It is the only key that goes into page code.
Add the script in your theme's functions.php or a code snippets plugin:
add_action('wp_head', function () {
echo '<script src="https://contactlint.com/widget.js" data-key="cl_pub_..." data-selector="input[type=email]" defer></script>';
});If you use a header code plugin instead, paste only the script tag. data-selector lets the widget find email fields you cannot add attributes to. To limit it to one form, use a narrower selector such as #signup-form input[type=email]. More options are in the framework setup guide.
If your form plugin has no server hook you trust, you can still hold back submission in the browser. Add this after the widget script:
<script>
document.addEventListener('contactlint:result', function (e) {
var r = e.detail;
var bad = r.verdict === 'invalid' || (r.disposable && !r.did_you_mean);
e.target.setCustomValidity(bad ? 'Please use a permanent, working email address.' : '');
});
document.addEventListener('input', function (e) {
if (e.target.type === 'email') e.target.setCustomValidity('');
});
</script>The second listener clears the block whenever the person edits the field. That way a failed check never leaves the form stuck.
Step 2: add the server-side check
Set an environment variable named CONTACTLINT_KEY to your secret key in your hosting panel or server config. Keep it out of theme files and version control. Then add this to functions.php or a code snippets plugin:
function contactlint_check_email($email) {
$key = getenv('CONTACTLINT_KEY');
if (!$key) {
return array('ok' => true); // no key set: fail open
}
$response = wp_remote_post('https://contactlint.com/v1/email/validate', array(
'timeout' => 3,
'headers' => array(
'Authorization' => 'Bearer ' . $key,
'Content-Type' => 'application/json',
),
'body' => wp_json_encode(array('email' => $email)),
));
if (is_wp_error($response) || (int) wp_remote_retrieve_response_code($response) !== 200) {
return array('ok' => true); // timeout, quota or other error: fail open
}
$data = json_decode(wp_remote_retrieve_body($response), true);
if (empty($data['result'])) {
return array('ok' => true);
}
$r = $data['result'];
if ($r['verdict'] === 'invalid') {
return array('ok' => false, 'message' => 'That email address cannot receive mail.');
}
if (!empty($r['did_you_mean'])) {
return array('ok' => false, 'message' => 'Did you mean ' . $r['did_you_mean'] . '?');
}
if (!empty($r['disposable'])) {
return array('ok' => false, 'message' => 'Please use a permanent email address.');
}
return array('ok' => true);
}
// Core WordPress registration form
add_filter('registration_errors', function ($errors, $sanitized_user_login, $user_email) {
$check = contactlint_check_email($user_email);
if (!$check['ok']) {
$errors->add('contactlint', esc_html($check['message']));
}
return $errors;
}, 10, 3);
// Contact Form 7 email fields, required and optional
function contactlint_cf7_validate($result, $tag) {
$value = isset($_POST[$tag->name]) ? trim(wp_unslash($_POST[$tag->name])) : '';
if ($value !== '') {
$check = contactlint_check_email($value);
if (!$check['ok']) {
$result->invalidate($tag, $check['message']);
}
}
return $result;
}
add_filter('wpcf7_validate_email*', 'contactlint_cf7_validate', 20, 2);
add_filter('wpcf7_validate_email', 'contactlint_cf7_validate', 20, 2);The 3 second timeout and the early return array('ok' => true) lines make this fail open. If ContactLint is slow, unreachable, or returns an error such as 402 quota_exceeded, the signup goes through.
Handling each verdict and did_you_mean
- invalid: the address is malformed or its domain has no MX or A record. WordPress shows the message in the registration error box. Contact Form 7 shows it under the field.
- risky with reason
possible_typo:did_you_meanholds the fix, so the person sees "Did you mean jane@gmail.com?" and can correct the field. The code checksdid_you_meanbeforedisposablebecause gmial.com is both a typo and on the disposable list. If you would rather let people keep a flagged spelling, remove that branch and rely on the widget hint. - risky with reason
disposable: rejected here. Remove that branch if you do not need a permanent address. - risky with reason
role_address(info@, sales@): accepted. It is a real, shared inbox. - valid or unknown: accepted. An unknown caused by a failed DNS lookup is not billed.
Testing it
- Open your registration or contact form, type
jane@gmial.comand click out of the field. The widget should show "Did you mean jane@gmail.com?". - Submit
jane@gmial.comanyway. The server check should return "Did you mean jane@gmail.com?" as a form error. - Submit
x@mailinator.com. You should see "Please use a permanent email address." - Submit
jane@example.invalid. It should be rejected as unable to receive mail. - Test fail open: set
CONTACTLINT_KEYto a wrong value. The API returns 401 and the signup should go through. Put the real key back.
If the widget stays silent, check that your exact origin (including www or not) is in the publishable key's origin list.
Limits
- ContactLint does not connect to mail servers. It cannot confirm that a mailbox exists or detect catch-all domains. It checks syntax, MX and A records, disposable domains, role addresses and typos of major providers.
- The widget never blocks a form by itself. Blocking is up to your code.
- With both the widget and the server check, one signup usually uses two checks. Every key gets 100 free checks per month, then credit packs from $9 for 5,000.
- Publishable keys allow 10 checks per minute per visitor network, 30 per minute per key, and the daily cap you set. The key is visible in page source, so set the cap to what you are willing to spend in a day.
- Typed addresses are sent to contactlint.com. Mention this in your privacy notice.
More detail: server-side validation and the widget guide.
Questions
Does this work with WooCommerce or other form plugins?
The widget works on any email input that matches your data-selector, including checkout fields. The server code here only hooks core registration and Contact Form 7. For other plugins, use the contactlint:result event in the browser, or add the helper function to that plugin's own validation hook from its documentation.
Can I put my secret key in the widget script?
No. Page code is public. Use a cl_pub_ publishable key in the script tag and keep the secret key in the CONTACTLINT_KEY environment variable on your server.
What happens if ContactLint is down?
The request times out after 3 seconds, the helper returns ok, and the signup goes through. The widget shows no hint, and the form still submits.
Other integrations
Get a free API key. 100 checks per month, no card required. Or try a live check first.