Email Verification at WooCommerce Checkout: Stop Fake Orders and Invalid Billing Emails
WooCommerce validates email format at checkout - it checks for an @ symbol and a basic domain pattern. That is all. It does not check whether the address actually exists, whether the domain has a working mail server, or whether the address is a throwaway from a disposable email service. Plenty of fake, mistyped, and fraudulent email addresses pass that format check just fine.
The result lands in your order management dashboard as a problem you have to solve manually: an order with a confirmed payment but an undeliverable billing email, a customer who calls support asking where their receipt is, or a queue of fraudulent COD orders placed with temporary inboxes that will never respond to fulfillment attempts.
Adding SMTP-level verification at checkout - the kind that actually confirms the mailbox exists - solves both the honest typo problem and the deliberate abuse problem. This guide covers the WooCommerce hooks that fire during checkout, how to call the Bulk Email Checker API from those hooks, and what to do with each verification result so you are not blocking legitimate customers who happen to use catch-all corporate email domains.
Why WooCommerce Default Validation Is Not Enough
WooCommerce's built-in email check uses PHP's filter_var($email, FILTER_VALIDATE_EMAIL) plus a regex pass. This catches obvious syntax errors: missing @, invalid characters, no TLD. It does not do anything beyond that.
An address like fakeorder@mailinator.com passes every WooCommerce validation check. So does nobody@10minutemail.com. So does sdfkjh@gmail.com - a gibberish address with valid syntax pointing at a domain that exists but has no mailbox with that local part. All three will generate an order, trigger all the fulfillment logic, and land in your orders list with no way to reach the customer.
The Two Checkout Email Problems
It is worth separating these because they call for different responses in your code.
Problem 1: Honest typos. A real customer types john@gmial.com instead of john@gmail.com. They genuinely want their order confirmation. The right response here is not to block the order - it is to surface the likely correction (the API returns this in the emailSuggested field) and ask them to confirm. Blocking a real customer over a typo creates support friction without protecting you from anything.
Problem 2: Fake and disposable addresses. These come from bots, serial returners, coupon abusers, and COD fraudsters who know they will not actually pay on delivery. The right response here is a hard block with a clear error message. No nuance, no suggestion, no path forward until they use a real address.
Good checkout verification handles both - it is not just about blocking, it is also about helping honest customers fix mistakes before they submit.
Which WooCommerce Hooks to Use
WooCommerce provides several hooks that fire during checkout processing. Two are most useful for email verification:
woocommerce_checkout_process fires when the customer clicks "Place Order" and WooCommerce begins validating the checkout form. Adding errors in this hook prevents the order from being created. This is the primary hook for blocking invalid emails at checkout.
woocommerce_after_checkout_validation fires after WooCommerce's own validation runs, giving you access to the posted data and existing error set. Useful if you want to add your verification after WooCommerce's built-in checks pass.
For real-time inline feedback before the form is submitted - the kind that validates as the customer types - you need a JavaScript AJAX approach, which the implementation section below covers alongside the server-side hook.
Implementation: Checkout Email Verification Plugin
The complete implementation below is structured as a standalone WordPress plugin. Drop it into wp-content/plugins/bec-checkout-verify/ as a single PHP file and activate it. Do not add this to functions.php - plugin-based customizations survive theme updates and are easier to disable when troubleshooting.
BEC_API_KEY, "email" => $email ], BEC_API_ENDPOINT );
$response = wp_remote_get( $url, [ "timeout" => 8 ] );
if ( is_wp_error( $response ) ) {
return null; // Network error - fail open
}
$code = wp_remote_retrieve_response_code( $response );
if ( $code !== 200 ) {
return null;
}
$data = json_decode( wp_remote_retrieve_body( $response ), true );
if ( $data ) {
wp_cache_set( $cacheKey, $data, "bec", HOUR_IN_SECONDS );
}
return $data;
}
// -----------------------------------------------------------------------
// Hook: server-side validation when Place Order is clicked
// -----------------------------------------------------------------------
add_action( "woocommerce_checkout_process", "bec_validate_checkout_email" );
function bec_validate_checkout_email(): void {
$email = isset( $_POST["billing_email"] )
? sanitize_email( wp_unslash( $_POST["billing_email"] ) )
: "";
if ( empty( $email ) ) {
return; // WooCommerce's own required-field check will catch this
}
$result = bec_woo_verify_email( $email );
if ( $result === null ) {
return; // API unavailable - fail open, allow the order
}
$status = $result["status"] ?? "unknown";
// Block disposable addresses - hard stop, no exceptions
if ( ! empty( $result["isDisposable"] ) ) {
wc_add_notice(
__( "Please use a permanent email address. Temporary or disposable email addresses are not accepted for orders.", "bec" ),
"error"
);
return;
}
// Block gibberish - likely a bot or deliberate junk entry
if ( ! empty( $result["isGibberish"] ) ) {
wc_add_notice(
__( "The email address you entered doesn't appear to be valid. Please double-check it and try again.", "bec" ),
"error"
);
return;
}
// Block addresses that confirmed as invalid (SMTP failed)
if ( $status === "failed" ) {
$suggested = $result["emailSuggested"] ?? null;
if ( $suggested ) {
/* translators: %s: suggested correct email address */
$msg = sprintf(
__( "That email address could not be verified. Did you mean %s?", "bec" ),
esc_html( $suggested )
);
} else {
$msg = __( "That email address could not be verified. Please check it and try again.", "bec" );
}
wc_add_notice( $msg, "error" );
return;
}
// Unknown status (catch-all domain) - allow through but log for review
// Remove this block if you want stricter behaviour
if ( $status === "unknown" ) {
$event = $result["event"] ?? "";
error_log( "BEC: catch-all/unknown email at checkout: $email (event: $event)" );
// Optionally store a flag on the session for post-order review
WC()->session->set( "bec_email_catchall", true );
}
}
// -----------------------------------------------------------------------
// Hook: attach verification metadata to order for audit trail
// -----------------------------------------------------------------------
add_action( "woocommerce_checkout_order_created", "bec_save_verification_to_order" );
function bec_save_verification_to_order( WC_Order $order ): void {
$email = $order->get_billing_email();
$result = bec_woo_verify_email( $email );
if ( $result ) {
$order->update_meta_data( "_bec_email_status", $result["status"] ?? "unknown" );
$order->update_meta_data( "_bec_email_event", $result["event"] ?? "" );
$order->update_meta_data( "_bec_is_disposable", ! empty( $result["isDisposable"] ) ? "yes" : "no" );
$order->update_meta_data( "_bec_is_role_account", ! empty( $result["isRoleAccount"] ) ? "yes" : "no" );
$order->update_meta_data( "_bec_mx_hostname", $result["mxEnrichment"]["mxHostname"] ?? "" );
$order->save();
}
}
// -----------------------------------------------------------------------
// AJAX endpoint for inline real-time validation as the customer types
// -----------------------------------------------------------------------
add_action( "wp_ajax_nopriv_bec_verify_ajax", "bec_ajax_verify" );
add_action( "wp_ajax_bec_verify_ajax", "bec_ajax_verify" );
function bec_ajax_verify(): void {
check_ajax_referer( "bec_verify_nonce", "nonce" );
$email = sanitize_email( wp_unslash( $_POST["email"] ?? "" ) );
if ( empty( $email ) || ! is_email( $email ) ) {
wp_send_json( [ "valid" => false, "message" => "Invalid email format." ] );
}
$result = bec_woo_verify_email( $email );
$status = $result["status"] ?? "unknown";
$suggested = $result["emailSuggested"] ?? null;
$disposable = ! empty( $result["isDisposable"] );
$valid = ( $status !== "failed" && ! $disposable && empty( $result["isGibberish"] ) );
$message = "";
if ( ! $valid ) {
$message = $suggested
? "Did you mean $suggested?"
: "This email address doesn't look right.";
}
wp_send_json( [
"valid" => $valid,
"message" => $message,
"isDisposable"=> $disposable,
"suggested" => $suggested,
] );
}
// -----------------------------------------------------------------------
// Enqueue inline JS for real-time field feedback
// -----------------------------------------------------------------------
add_action( "wp_enqueue_scripts", "bec_enqueue_checkout_js" );
function bec_enqueue_checkout_js(): void {
if ( ! is_checkout() ) return;
wp_add_inline_script( "jquery", "
jQuery(function($) {
var debounceTimer;
var nonce = '" . wp_create_nonce( "bec_verify_nonce" ) . "';
$('body').on('input blur', '#billing_email', function() {
clearTimeout(debounceTimer);
var email = $(this).val().trim();
var field = $(this).closest('.form-row');
var notice = field.find('.bec-inline-notice');
if (notice.length === 0) {
notice = $('');
field.append(notice);
}
if (email.length < 6 || email.indexOf('@') === -1) {
notice.text('').removeAttr('data-valid');
return;
}
debounceTimer = setTimeout(function() {
$.post('" . admin_url( "admin-ajax.php" ) . "', {
action: 'bec_verify_ajax',
nonce: nonce,
email: email
}, function(resp) {
if (resp.valid) {
notice.css('color', '#16a34a').text('✓ Email verified');
} else {
notice.css('color', '#dc2626').text(resp.message || 'Please check this email address.');
}
});
}, 700);
});
});
" );
}
A few notes on the implementation above:
- It uses
wp_remote_get()instead of cURL directly, so it respects WordPress proxy settings and SSL configuration automatically - Results are cached in the WordPress object cache for one hour - if the same email triggers checkout twice (double-clicks, back/forward navigation), it does not burn an extra API credit
- Verification metadata is saved to the order itself as custom meta fields, so you can filter and sort orders by email quality in WooCommerce admin
- The inline JavaScript adds visual confirmation to the billing email field - a green "✓ Email verified" message for valid addresses, a red warning for invalid ones - before the form is submitted
Handling Corporate Catch-All Domains
One thing stores often get wrong when implementing checkout verification: blocking catch-all domains entirely. Many large enterprise customers use company email domains that accept all incoming mail regardless of whether the specific mailbox exists. An SMTP verification call returns unknown or the event is_catchall for these - not passed, because the server would not confirm or deny the specific address.
If you block all unknown status addresses, you will prevent legitimate B2B orders from customers at large companies. The plugin above logs catch-all detections and saves a session flag, but allows the order through. This is the right default for most stores.
failed, isDisposable, and isGibberish at checkout.
Extra Caution for Digital Downloads and COD
Two store types have higher stakes for checkout email verification:
Digital download stores: When an order is placed and paid, the download link goes to the billing email immediately. If that email is fake, the customer never gets their product and opens a dispute. For digital-only stores, consider tightening the unknown handling - you may want to block catch-all addresses and ask the customer to contact you for delivery if their domain produces an inconclusive result.
Cash on delivery stores: COD orders are placed without payment. A fraudster can place unlimited COD orders with disposable or fake emails, causing your fulfilment team to prepare shipments that will never be picked up or paid for. For stores with COD enabled, blocking disposable emails at checkout is especially important. The plugin code above already handles this with a hard block on isDisposable = true.
_bec_email_status, _bec_is_disposable) to create a WooCommerce admin custom column. Install the "Admin Columns" plugin or add a simple filter - you can then spot orders with suspicious email status at a glance in your Orders list without opening each one.
You can add an admin column with a simple filter in the same plugin:
get_meta( "_bec_email_status" );
$disposable = $order->get_meta( "_bec_is_disposable" );
if ( $disposable === "yes" ) {
echo '⚠ Disposable';
} elseif ( $status === "passed" ) {
echo '✓ Passed';
} elseif ( $status === "unknown" ) {
echo '~ Catch-all';
} elseif ( $status === "failed" ) {
echo '✕ Failed';
} else {
echo '—';
}
}
Frequently Asked Questions
Will this slow down checkout for my customers?
The API call happens server-side when the customer clicks "Place Order." The timeout is set to 8 seconds in the plugin above. In practice, verification typically completes in under 500ms. The inline JavaScript validation (the real-time field feedback) is purely asynchronous and does not block the form. If your store is in a geographic location where the API consistently responds slowly, increase the timeout value or cache results more aggressively.
What if the API is unavailable when a customer checks out?
The plugin is coded to fail open - if wp_remote_get() returns a WP error or the API returns a non-200 status, the checkout proceeds normally. The last thing you want is a payment processing failure caused by a third-party API outage. Log the error and review flagged orders manually if needed.
Should I block role-based addresses like info@ at checkout?
Generally no, not at WooCommerce checkout. Role accounts like orders@company.com are a legitimate billing email for many B2B purchases - a procurement department or accounts payable team often places orders using a shared inbox. Block role accounts at newsletter signup and lead gen forms where personal engagement is the goal; allow them at checkout where you just need order communication to reach someone at the company.
Can I use this alongside the WooCommerce "Customer Email Verification" plugin?
Yes. That plugin sends an OTP to confirm the customer owns the address - a different check than SMTP deliverability verification. They are complementary: this plugin confirms the address exists at checkout, the OTP plugin confirms the customer can receive email there. If you run both, you get the strongest possible guarantee that order communications will reach a real inbox.
Where can I see my API usage and remaining credits?
The API response includes a creditsRemaining field in every call. The plugin logs this for catch-all addresses. You can also check your account dashboard at BulkEmailChecker.com pricing to review usage and top up credits, or switch to the unlimited plan if checkout volume is high enough to make per-credit billing inconvenient.
Better Data From Every Order
Adding verification at checkout does two things at once: it protects your store from fake and fraudulent orders, and it improves the quality of your customer database from day one. Every order that completes with a verified billing email is an order where you can reach the customer - for receipts, shipping updates, re-engagement campaigns, and everything else that depends on reliable contact data.
The implementation above takes about 15 minutes to deploy. Install the plugin, add your API key from the Bulk Email Checker API, activate it, and test with a known-bad address like test@mailinator.com to confirm the block is working. Then turn it on and let it run quietly in the background.
Stop Bouncing. Start Converting.
Millions of emails verified daily. Industry-leading SMTP validation engine.