# How to validate email addresses in WordPress

_2026-10-28 · Boundstone (https://boundstone.io/blog/validate-email-wordpress)_


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](/tools/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.

```php
// 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

```php
// 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:

```json
{
  "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 with `wp_remote_post()`.
- Put `bs_live_` in `wp-config.php`, never in a form field.
- Reject on `disposable`, warn on a missing `mx_found`, and decide your own policy on `role_account`.
- Read `checks.not_performed` — `smtp_mailbox` and `catch_all` aren'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](/docs). If you want the same logic outside WordPress, the [PHP guide](/blog/validate-email-php) does it without the WordPress helpers.
