Production-ready PHP code for BounceZero email verification - cURL single request, Guzzle HTTP client, Laravel custom validation rule, WordPress registration hook, WooCommerce checkout guard, and batch CSV processing.
<?php
$apiKey = getenv("BOUNCEZERO_API_KEY");
$email = "[email protected]";
function verifyEmail(string $email, string $apiKey): ?object {
$ch = curl_init("https://api.bouncezero.io/v1/verify");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode(["email" => $email]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer " . $apiKey,
"Content-Type: application/json",
],
CURLOPT_TIMEOUT => 10,
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($status !== 200 || !$response) {
return null; // fail open - do not block signup
}
return json_decode($response);
}
$result = verifyEmail($email, $apiKey);
if ($result) {
echo $result->result; // "valid" | "invalid" | "unknown" | "catch-all"
echo $result->score; // 0.0-1.0
echo $result->is_disposable ? "disposable" : "not disposable";
echo $result->mx_valid ? "mx ok" : "no mx";
}
<?php
use GuzzleHttp\Client;
$apiKey = getenv("BOUNCEZERO_API_KEY");
$client = new Client(["base_uri" => "https://api.bouncezero.io"]);
function verifyEmail(string $email, string $apiKey, Client $client): ?object {
try {
$response = $client->post("/v1/verify", [
"headers" => [
"Authorization" => "Bearer " . $apiKey,
"Content-Type" => "application/json",
],
"json" => ["email" => $email],
"timeout" => 10,
]);
return json_decode((string) $response->getBody());
} catch (\Exception $e) {
return null; // fail open
}
}
$result = verifyEmail("[email protected]", $apiKey, $client);
echo $result?->result; // "valid"
<?php
namespace App\Rules;
use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Support\Facades\Http;
class BounceZeroEmail implements ValidationRule
{
public function validate(string $attribute, mixed $value, Closure $fail): void
{
try {
$response = Http::timeout(10)
->withToken(config("services.bouncezero.key"))
->post("https://api.bouncezero.io/v1/verify", ["email" => $value])
->object();
if ($response?->result === "invalid") {
$fail("The :attribute does not appear to be a valid email address.");
}
if ($response?->is_disposable) {
$fail("Please use a work or personal email address.");
}
} catch (\Exception) {
// Fail open - never block signup on API error
}
}
}
// In config/services.php:
// "bouncezero" => ["key" => env("BOUNCEZERO_API_KEY")],
// In your Form Request:
// "email" => ["required", "email", new \App\Rules\BounceZeroEmail],
<?php
add_filter("registration_errors", "bz_verify_registration_email", 10, 3);
function bz_verify_registration_email(WP_Error $errors, string $sanitized_user_login, string $user_email): WP_Error
{
$api_key = defined("BOUNCEZERO_API_KEY") ? BOUNCEZERO_API_KEY : "";
if (empty($api_key) || $errors->has_errors()) {
return $errors; // skip if key not set or earlier errors present
}
$response = wp_remote_post("https://api.bouncezero.io/v1/verify", [
"headers" => [
"Authorization" => "Bearer " . $api_key,
"Content-Type" => "application/json",
],
"body" => json_encode(["email" => $user_email]),
"timeout" => 10,
]);
if (is_wp_error($response)) {
return $errors; // fail open
}
$body = json_decode(wp_remote_retrieve_body($response));
if (isset($body->result) && $body->result === "invalid") {
$errors->add("invalid_email", __("<strong>Error</strong>: That email address doesn't appear to be valid."));
}
if (!empty($body->is_disposable)) {
$errors->add("disposable_email", __("<strong>Error</strong>: Please use a work or personal email address."));
}
return $errors;
}
// In wp-config.php:
// define("BOUNCEZERO_API_KEY", "your_api_key_here");
| Field | Type | Values | Use for |
|---|---|---|---|
| result | string | valid / invalid / unknown / catch-all | Primary routing - suppress invalid |
| score | float | 0.0 - 1.0 | Threshold filtering for catch-all decisions |
| is_disposable | bool | true / false | Block throwaway addresses at signup |
| is_role_address | bool | true / false | Suppress info@/admin@ in personal campaigns |
| mx_valid | bool | true / false | Domain has active mail server |
| provider | string | “Gmail” / “Outlook” / etc. | Provider-specific routing or personalisation |
Use cURL or Guzzle to POST to https://api.bouncezero.io/v1/verify with your API key in the Authorization header and {email: address} as JSON. Decode the response with json_decode() and check and . Always fail open on API errors (return null and continue) to avoid blocking legitimate signups.
Create a custom Rule class (php artisan make:rule BounceZeroEmail) that calls BounceZero via Laravel’s Http facade in the validate() method. Return () if result is ‘invalid’ or is_disposable is true. Add new BounceZeroEmail to your Form Request rules for the email field.
Use the registration_errors filter hook. Call BounceZero with wp_remote_post() and check the response body. If invalid or disposable, add a WP_Error with (). WordPress blocks registration and displays the error. Always fail open (return unchanged) on wp_remote_post failures.
100 free credits on signup. No credit card. Instant access. $3/1K thereafter - credits never expire. Works with any PHP 7.4+ application.
Deep-dive guides on how email verification and inbox placement work
272,446-domain census: DMARC gap, provider divide, catch-all rates
10.2M verifications: 12.3% of addresses are dead, and where they hide
826K re-verifications: only 19% of valid addresses survive 90 days
True catch-all is 1.4% - most of what looks catch-all is unprobeable providers
info@ bounces 4.5x more than personal addresses - measured, not guessed
The 3x invalid-rate gap that vanishes when you control for domain size
Explore other topics