Key Takeaways

  • Laravel Form Request validation is the cleanest place to plug in real-time email verification. Custom rule, single line, every signup gated.
  • The v2 verify endpoint returns enough fields to make decisions on disposable, role-account, and gibberish detection in addition to mailbox existence.
  • Production deployments need three additions: a custom rule for sync verification, a queued job for bulk re-verification, and graceful degradation when the API is briefly unreachable.
  • The approach works on plain PHP, Laravel, Symfony, and CodeIgniter with minor adjustments to the HTTP client.

Laravel and PHP power a vast share of the web. WordPress, Magento, Shopware, custom Laravel applications, and the long tail of legacy PHP all share the same need: validating email addresses before they enter the database. The native PHP FILTER_VALIDATE_EMAIL and Laravel's built-in email validator both check syntax. Neither verifies that the mailbox actually exists, that the domain accepts mail, or that the address is not a disposable address from a temporary mail service. This guide covers the production pattern that adds those checks at the validation layer.

The complete reference implementation is documented on the verify email with PHP integration page, including framework-specific patterns for Laravel, Symfony, and plain PHP.

The v2 Endpoint

Before writing PHP, confirm the endpoint works against a known address. The v2 verify endpoint accepts the email as a query parameter and returns a JSON document with all the fields needed for decision logic.

curl -X GET 
  "https://emailverifierapi.com/v2/verify?api_key=YOUR_API_KEY&email=jane@example.com"

# Response
{
  "email": "jane@example.com",
  "status": "passed",
  "sub_status": "mailboxExists",
  "isDisposable": false,
  "isFreeService": false,
  "isOffensive": false,
  "isRoleAccount": false,
  "isGibberish": false,
  "smtp_check": "success"
}

The status field is the primary decision input. Passed indicates the mailbox exists and accepts mail. Failed means the mailbox does not exist, syntax is invalid, or the domain has no MX server. Unknown covers greylisting and transient SMTP errors. The boolean flags surface category-specific risk signals that warrant separate handling.

The Laravel Custom Validation Rule

Laravel's invokable rule pattern is the cleanest way to plug verification into existing validators. The rule wraps the HTTP call and returns a structured failure when the address does not pass.

<?php
// app/Rules/VerifiedEmail.php
namespace AppRules;

use Closure;
use IlluminateContractsValidationValidationRule;
use IlluminateSupportFacadesCache;
use IlluminateSupportFacadesHttp;
use IlluminateSupportFacadesLog;

class VerifiedEmail implements ValidationRule
{
    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        $cacheKey = 'verify:' . md5(strtolower($value));

        $result = Cache::remember($cacheKey, 1800, function () use ($value) {
            try {
                $response = Http::timeout(5)
                    ->retry(2, 200)
                    ->get('https://emailverifierapi.com/v2/verify', [
                        'api_key' => config('services.emailverifier.key'),
                        'email'   => $value,
                    ]);

                return $response->json();
            } catch (Throwable $e) {
                Log::warning('Email verify failed', ['e' => $e->getMessage()]);
                return ['status' => 'unknown'];
            }
        });

        if (($result['status'] ?? 'unknown') === 'failed') {
            $fail('The :attribute is not deliverable.');
            return;
        }

        if ($result['isDisposable'] ?? false) {
            $fail('Please use a non-disposable email address.');
            return;
        }

        if ($result['isGibberish'] ?? false) {
            $fail('Please double-check your email address.');
            return;
        }
    }
}

Two design decisions deserve attention. The rule caches verification results for 30 minutes per email to avoid duplicate API calls during retries and back-button replays. The rule fails closed only on definitive negative results: status equals failed, disposable equals true, or gibberish equals true. Status equals unknown is allowed through, and a queued job re-verifies asynchronously. The application stays online if the verification API is briefly unavailable.

Pro Tip Laravel's Http::retry() uses exponential backoff by default. For verification calls, two retries with 200ms initial delay is enough to handle transient network blips without making slow signups feel slower.

Mounting the Rule on a Form Request

With the rule defined, the Form Request gets a single-line addition. The rule runs alongside Laravel's standard email validator, providing both syntax and deliverability checks in one validation pass.

<?php
// app/Http/Requests/SignupRequest.php
namespace AppHttpRequests;

use AppRulesVerifiedEmail;
use IlluminateFoundationHttpFormRequest;

class SignupRequest extends FormRequest
{
    public function rules(): array
    {
        return [
            'name'     => ['required', 'string', 'max:120'],
            'email'    => ['required', 'email:rfc', new VerifiedEmail],
            'password' => ['required', 'string', 'min:8'],
        ];
    }
}

The controller signature consumes the validated request without further verification logic. The validation layer is the gate, and the controller assumes the address has passed.

85% reduction in welcome-email bounces after Form Request verification. Source: 2025 deployment data across Laravel SaaS applications

Bulk Verification with a Queued Job

Real-time verification handles new signups. Existing users predate the validator and need bulk re-verification. The Laravel queue is the right place for this work because verification of a large user base should not block any web request.

<?php
// app/Jobs/VerifyUserEmail.php
namespace AppJobs;

use AppModelsUser;
use IlluminateBusQueueable;
use IlluminateContractsQueueShouldQueue;
use IlluminateFoundationBusDispatchable;
use IlluminateQueueInteractsWithQueue;
use IlluminateQueueSerializesModels;
use IlluminateSupportFacadesHttp;

class VerifyUserEmail implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public int $tries = 3;
    public int $backoff = 30;

    public function __construct(public int $userId) {}

    public function handle(): void
    {
        $user = User::find($this->userId);
        if (!$user || !$user->email) return;

        $r = Http::timeout(8)
            ->get('https://emailverifierapi.com/v2/verify', [
                'api_key' => config('services.emailverifier.key'),
                'email'   => $user->email,
            ])
            ->json();

        $user->update([
            'email_status'       => $r['status'] ?? 'unknown',
            'email_is_disposable'=> (bool) ($r['isDisposable'] ?? false),
            'email_is_role'      => (bool) ($r['isRoleAccount'] ?? false),
            'email_verified_at'  => now(),
        ]);
    }
}

Schedule the job through Laravel's scheduler or dispatch in chunks via a console command. A typical 100,000-user base verifies in 60 to 90 minutes through standard queue workers. Larger user bases benefit from the dedicated bulk endpoint, which is documented in the email verification API documentation.

For a broader catalog of integration patterns across other PHP frameworks and languages, the email validation API integrations hub covers Symfony, CodeIgniter, plain PHP, and the rest of the supported stacks. New developers can grab 100 free email verification credits on signup to test integration before paying for a plan.

Frequently Asked Questions

Should the API key live in config or env?

Both. Set the value in .env, surface it through config/services.php, and reference config('services.emailverifier.key') everywhere it is used. This is Laravel convention and supports cache:config in production.

What if the verification API is unreachable?

The rule treats unknown as soft failure. The signup proceeds and a queued job re-verifies once the API recovers. Fail-closed is the wrong default because it converts a transient infrastructure problem into a customer-facing outage.

Does this work outside Laravel?

Yes. The same pattern (cache, retry, fail-soft) applies in Symfony with HttpClientInterface, in CodeIgniter with the Services library, or in plain PHP with cURL. The verify call is a single HTTP GET; the framework wrapping is the only thing that changes.

How should I handle role accounts?

Allow them but flag in the user record. Marketing emails should suppress role accounts because they generate higher complaint rates. Transactional mail to role accounts is generally fine.