Glasser
← All posts

Glasser Team8 min read

Email Verification API Examples in Node.js and PHP

Implement an email verification API in Node.js or PHP. Handle pending requests, uncertain results and service failures without losing contact data.

An ivory envelope passes through an amber arch toward check, question and cross tiles.

What does this email verification API example check?

An email verification API takes an address you already have and returns evidence about its deliverability. This guide implements a small server-side client in Node.js and PHP, then keeps mailbox verdicts separate from pending work and service failures.

Use this flow when importing contacts or checking an address submitted through a form. If you need to prove that a user controls the inbox, add a separate confirmation-link or code flow. A deliverability check cannot establish ownership.

The examples use Hunter's direct Email Verifier API. Its public documentation was checked on October 9, 2026. The Node.js client passed offline response tests; the PHP version received static review only. Neither example was sent to the live service.

What do you need before making a request?

Choose the access path for your application

Glasser's Hunter verification operation accepts an existing email address. If you already use Glasser for lead data, inspect that operation's current input and charging terms before introducing another provider account. The public catalog does not publish a guaranteed output schema, so confirm the returned structure before mapping fields.

The code below is for direct Hunter access. Prepare a Hunter API key, keep it in the server environment as HUNTER_API_KEY, and supply an address you are authorized to check as EMAIL_TO_CHECK. Use Node.js 22 or later, or PHP 8.2 or later with the cURL extension. These are the runtime assumptions for these examples.

Keep the request on the server

The client calls GET /v2/email-verifier with an encoded email parameter and sends the credential in the documented X-API-KEY header. Keep credentials out of browser JavaScript and logs. Avoid logging full query URLs because they contain the address being checked. Hunter authentication and verifier documentation

Hunter documents a verification wait of up to 20 seconds before returning HTTP 202. The examples allow 30 seconds for one request and return a pending state immediately on 202. Any later polling belongs in your application, with a bounded attempt count and delay.

How do you call the API from Node.js and PHP?

Save the Node.js client

Save this as verify-email.mjs. The optional fetcher argument allows offline response testing; normal calls use Node's built-in fetch.

export async function verifyEmail(email, apiKey, fetcher = fetch) {
  if (!email?.trim() || !apiKey?.trim()) {
    throw new Error('Email and HUNTER_API_KEY are required');
  }
  const url = new URL('https://api.hunter.io/v2/email-verifier');
  url.searchParams.set('email', email.trim());
  const signal = AbortSignal.timeout(30_000);
  let response;
  try {
    response = await fetcher(url, {
      headers: { 'X-API-KEY': apiKey, Accept: 'application/json' },
      signal,
      redirect: 'error',
    });
  } catch {
    throw new Error(signal.aborted ? 'Verification timed out' : 'Verification transport failed');
  }
  if (response.status === 202) return { state: 'pending' };
  if (response.status !== 200) {
    throw new Error(`Verification HTTP ${response.status}`);
  }
  let payload;
  try {
    payload = await response.json();
  } catch {
    throw new Error(signal.aborted ? 'Verification timed out' : 'Unreadable verification response');
  }
  const statuses = ['valid', 'invalid', 'accept_all', 'webmail', 'disposable', 'unknown'];
  if (!statuses.includes(payload?.data?.status)) {
    throw new Error('Unexpected verification schema');
  }
  return { state: 'complete', status: payload.data.status, raw: payload.data };
}

In another file, check-email.mjs, import the function and report only the application state and verdict:

import { verifyEmail } from './verify-email.mjs';

try {
  const result = await verifyEmail(
    process.env.EMAIL_TO_CHECK,
    process.env.HUNTER_API_KEY,
  );
  console.log({ state: result.state, status: result.status ?? null });
} catch (error) {
  console.error(error.message);
  process.exitCode = 1;
}

After setting the two environment variables privately, run node check-email.mjs. A pending result leaves the verdict empty. Schedule a later call with the same address if your workflow needs to finish the check.

Save the PHP client

Save this as verify-email.php. It applies the same response rules using PHP's cURL and JSON support.

<?php
function verifyEmail(string $email, string $apiKey): array {
    if (trim($email) === '' || trim($apiKey) === '') {
        throw new RuntimeException('Email and HUNTER_API_KEY are required');
    }
    $url = 'https://api.hunter.io/v2/email-verifier?' . http_build_query(
        ['email' => trim($email)], '', '&', PHP_QUERY_RFC3986
    );
    $ch = curl_init($url);
    if ($ch === false) throw new RuntimeException('Cannot initialize cURL');
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HTTPHEADER => ['X-API-KEY: ' . $apiKey, 'Accept: application/json'],
        CURLOPT_CONNECTTIMEOUT => 10,
        CURLOPT_TIMEOUT => 30,
        CURLOPT_FOLLOWLOCATION => false,
    ]);
    $body = curl_exec($ch);
    $code = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
    $error = curl_errno($ch);
    curl_close($ch);
    if ($body === false) {
        throw new RuntimeException($error === CURLE_OPERATION_TIMEDOUT
            ? 'Verification timed out' : 'Verification transport failed');
    }
    if ($code === 202) return ['state' => 'pending'];
    if ($code !== 200) throw new RuntimeException('Verification HTTP ' . $code);
    try {
        $payload = json_decode($body, true, 512, JSON_THROW_ON_ERROR);
    } catch (JsonException $e) {
        throw new RuntimeException('Unreadable verification response');
    }
    $data = is_array($payload) ? ($payload['data'] ?? null) : null;
    $status = is_array($data) ? ($data['status'] ?? null) : null;
    $statuses = ['valid', 'invalid', 'accept_all', 'webmail', 'disposable', 'unknown'];
    if (!in_array($status, $statuses, true)) {
        throw new RuntimeException('Unexpected verification schema');
    }
    return ['state' => 'complete', 'status' => $status, 'raw' => $data];
}

Call it from check-email.php:

<?php
require __DIR__ . '/verify-email.php';
try {
    $result = verifyEmail(
        getenv('EMAIL_TO_CHECK') ?: '',
        getenv('HUNTER_API_KEY') ?: ''
    );
    echo json_encode([
        'state' => $result['state'],
        'status' => $result['status'] ?? null,
    ], JSON_THROW_ON_ERROR) . PHP_EOL;
} catch (RuntimeException $e) {
    fwrite(STDERR, $e->getMessage() . PHP_EOL);
    exit(1);
}

With the environment variables set, run php check-email.php. Both clients accept HTTP 200 as a completed response and explicitly handle 202 before decoding. This prevents a generic “any 2xx is success” check from misreading Hunter's special 222 failure response.

How should your application handle verification results?

Preserve the provider verdict before applying a policy

Hunter's current field is data.status; its older result field is deprecated. The wrapper's state is our own application label. A completed request can still return an uncertain mailbox verdict.

Provider statusSuggested application treatment
validRetain the positive verdict and check date; apply existing contact preferences separately
invalidFlag the address for correction or exclusion under your address-quality rules
accept_allHold for review when you need mailbox-specific confidence
webmailApply your policy for personal email services; avoid automatically treating the category as invalid
disposableApply your explicit disposable-address policy
unknownPreserve uncertainty and choose whether a later check is useful

The statuses come from Hunter's verifier reference. The actions in the right column are editorial recommendations for your application. The code returns raw data so you can retain additional evidence without inventing a universal acceptance threshold.

Keep request time and verification time separate

Store the address, provider, request time, raw status, application decision, and provider verification date when supplied. Hunter can return a prior verification, so the request timestamp alone does not show when the mailbox was checked. Preserve missing dates as missing.

For form submissions, decide how users proceed when verification is pending or unavailable. For imports, keep unresolved rows in a review queue instead of silently discarding them. Never replace an existing address-quality decision with a network error.

How do you keep failures out of your contact decisions?

Diagnose the request before changing the record

ObservationNext action
HTTP 202Keep pending; schedule a bounded later poll of the same endpoint
HTTP 222Keep the SMTP-check failure separate; consider a later retry
HTTP 400Review the supplied address and request parameters
HTTP 401Resolve credential access
HTTP 403 or 429Check the provider's rate or usage limit and account state
HTTP 451Stop processing this address and apply the provider's restriction
HTTP 5xx, timeout or network failurePreserve the existing contact decision and investigate service availability
Invalid JSON or unexpected fieldsTreat the response as an integration problem and review the current schema

Hunter documents these HTTP conditions in its API reference. The clients deliberately avoid automatic retries. Add retry scheduling only after choosing a request budget, a maximum number of attempts and a policy for terminal errors.

If provider authentication and billing are the main integration burden, inspect Glasser's verification contract. Glasser documents one key and a Workspace balance for supported operations. Keep its execution status, returned data and charge handling aligned with the Glasser workflow documentation; the direct Hunter field mapping above needs separate validation before reuse.

Test decisions without sending real addresses

The accompanying Node.js client passed 23 offline tests covering all six verdicts, pending responses, HTTP errors, malformed payloads, plus-address encoding, missing credentials, transport failures and an injected aborted signal. The timeout test exercises the error branch without waiting 30 seconds. These fixtures establish client behavior under supplied responses; they do not measure API availability or verification accuracy.

The PHP client has not been executed because the review environment has no PHP runtime. Before production use, run it under your deployed PHP version, exercise the same failure cases, and perform an authorized service check with your own account. Also add application-level authentication, request limits and queue handling appropriate to your workload.

Frequently asked questions

Does this send a confirmation email?

These examples request a deliverability check. Use a separate confirmation message and token flow when the user must demonstrate access to an inbox.

Can I reject every unknown result?

An unknown verdict leaves the check unresolved. Decide whether to permit, review or recheck it based on your application policy, and retain that uncertainty in the record.

Can I paste this code into a browser form?

Keep the provider credential on your server. Have the form call your own authenticated and rate-limited backend rather than exposing the API key to visitors.

Can I reuse the same response mapping through Glasser?

Inspect the selected Glasser operation and an authorized sample result first. The direct API's data.status path is not a verified Glasser response contract.

Where can I compare free verification allowances?

The free email verification API guide covers provider selection and allowances. This implementation assumes you have chosen and configured your access.