How to validate email addresses in WordPress
WordPress's is_email() confirms an address is shaped right and nothing more; here's the functions.php hook that adds a real MX and disposable check.
Contents
WordPress ships with is_email(), and it is doing less than its name suggests. Hand it jane@example.com and it returns the address; hand it jane@example and it returns false. That is the whole job: it confirms the string is shaped like an email. It does not check whether the domain can receive mail, whether it's a ten-minute throwaway, or whether it's a role inbox that no human reads. For plenty of sites — a contact form, a low-stakes newsletter — syntax plus a confirmation email is genuinely all you need, and you can stop reading here. If you're tired of registration lists full of asdf@mailinator.com, add one network call.
What is_email() checks — and where it stops
is_email() validates format. It requires an @, checks the characters, and insists the domain has at least one dot. It runs entirely on the string in front of it — no DNS, no network, nothing about the world outside your server. That's a feature: it's fast, and it never blocks a signup because a lookup timed out. It's also the reason is_email() cannot tell a live domain from one that expired in 2019, or a corporate address from a burner.
If all you want is a browser-side sanity check with no key involved, the free email validator runs a syntax-plus-MX check from a form with nothing to install. To make a decision inside WordPress, you call the API server-side.
The plan: a hook, not a plugin
There is no official Boundstone plugin, and you don't need one. WordPress already gives you the seam: registration_errors for user signups, and an equivalent validation filter on every serious form plugin. You hook it, call the REST endpoint with wp_remote_post(), read the JSON, and add an error if the address is disposable or its domain is dead. That's the entire integration — a function in your theme's functions.php, or better, a small site-specific plugin so it survives theme changes.
Keep the key in wp-config.php
Your bs_live_ key is a server-side secret. It goes in wp-config.php as a constant, never in a form field, a shortcode attribute, or anything that reaches the browser.
// wp-config.php
define( 'BOUNDSTONE_KEY', 'bs_live_YOUR_KEY' );
Keep wp-config.php out of your repo. If you'd rather manage the key from the admin, store it with update_option() and read it with get_option() — same rule applies: it stays on the server.
Call the API from your registration hook
// functions.php — reject dead domains and disposables at registration
add_filter( 'registration_errors', 'bs_verify_email', 10, 3 );
function bs_verify_email( $errors, $login, $email ) {
// WordPress already ran is_email(); we go one layer past it.
$response = wp_remote_post( 'https://api.boundstone.io/v1/verify/email', array(
'timeout' => 8,
'headers' => array(
'Authorization' => 'Bearer ' . BOUNDSTONE_KEY,
'Content-Type' => 'application/json',
),
'body' => wp_json_encode( array( 'email' => $email ) ),
) );
// Network trouble on our side shouldn't cost you a signup.
if ( is_wp_error( $response ) ) {
return $errors;
}
$data = json_decode( wp_remote_retrieve_body( $response ), true );
if ( empty( $data['mx_found'] ) ) {
$errors->add( 'bs_no_mx', 'That domain can\'t receive email. Check the spelling.' );
}
if ( ! empty( $data['disposable'] ) ) {
$errors->add( 'bs_disposable', 'Please sign up with a permanent email address.' );
}
return $errors;
}
wp_remote_post() sends the request, wp_remote_retrieve_body() pulls the body out of the response array, and json_decode( …, true ) gives you an associative array. Note the is_wp_error() guard: if the call fails, you let the registration through rather than punishing a user for your outage. Decide for yourself whether a role account (role_account) should warn, block, or pass — info@ and sales@ are real addresses, just not people.
What a true means here — and what it doesn't
The response is deliberately literal about its own limits:
{
"valid_syntax": true,
"domain": "example.com",
"mx_found": true,
"disposable": false,
"role_account": false,
"free_provider": false,
"checks": {
"performed": ["syntax", "mx", "disposable_list", "role_list"],
"not_performed": ["smtp_mailbox", "catch_all"]
}
}
Read checks.not_performed before you trust anything. smtp_mailbox and catch_all are on it, which means Boundstone did not knock on the mailbox to confirm a human is behind it, and did not test whether the domain accepts every address thrown at it. So a clean result here means the syntax parses, the domain publishes MX records, it isn't on the disposable list, and it isn't a role inbox. It does not mean the mailbox exists. The only thing that proves someone reads an inbox is sending mail to it — your confirmation email still does the job it always did. Boundstone's job is to stop the obviously-bad addresses before they get that far. (free_provider just flags Gmail- and Yahoo-style consumer domains — it's a signal you can act on, not a verdict, and it isn't one of the checks performed.)
Form plugins: same call, different hook
Gravity Forms, WPForms, Formidable and the rest don't expose a native Boundstone step — they call the REST API through their own validation filter or a webhook, exactly like wp_remote_post() above. The pattern doesn't change: intercept the field on submit, POST the value to /v1/verify/email, read disposable and mx_found, and set a field error if either fails. A native integration is planned; until then it's the same few lines of glue.
The short version
is_email()is syntax only, and it's lenient — it never checks MX, disposable, or role.- There's no Boundstone plugin; hook
registration_errors(or your form plugin's validator) and call the API withwp_remote_post(). - Put
bs_live_inwp-config.php, never in a form field. - Reject on
disposable, warn on a missingmx_found, and decide your own policy onrole_account. - Read
checks.not_performed—smtp_mailboxandcatch_allaren't done, so keep your confirmation email. - The free tier is 250 credits a month, no card, and credits don't expire.
The full field list, bulk CSV, and rate limits are in the docs. If you want the same logic outside WordPress, the PHP guide does it without the WordPress helpers.