Tutorials

Verify email domains in Node.js, step by step

This tutorial builds one small function around the email domain check using only the global fetch. It handles the three outcomes you will meet in production: a normal answer (including found: false), a structured error, and a rate limit that needs a wait.

By LookValid engineering. Published 2026-10-19.

Step 1: send the request

Node ships a global fetch, based on the undici HTTP client 2. The check is one POST with a JSON body holding either email or domain, and your key in the Authorization header.

curl -X POST https://YOUR_API_HOST/v1/email-domain-check \
  -H "Authorization: Bearer $LOOKVALID_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"email":"info@example.com"}'

The request as curl, to test your key first

Step 2: know what fetch does and does not throw

A fetch() promise does not reject when the server answers with an error status such as 404 or 504. It rejects only when the request itself fails, for example on a network error, and a handler must check Response.ok or Response.status 1. So two kinds of failure need two code paths: an exception for the network, and a status check for everything the API says.

Step 3: treat found false as an answer

The API answers HTTP 200 whether or not it found something. For a domain that does not exist in DNS, the body has found: false and a reason, and nothing is charged. Do not route that through your error path: it is a result, and for a signup form it usually means "ask the person to retype the address".

Step 4: read the error envelope

Every failure uses one envelope: an error object with code, message, fix, docs_url, retryable and request_id. Branch on code, and use retryable rather than guessing from the status. Each code has a page under errors.

Error codes the API can return
CodeHTTPWhat to do
invalid_input400Fix the request; retrying as sent will not help
signature_invalid400Fix the signature; not retryable as sent
unauthorized401Send a valid key
insufficient_credits402Add credits, then retry
forbidden403The key may not do this; do not retry
unknown_product, not_found404Check the product id or the path
method_not_allowed405Use POST
conflict409Do not retry as sent
payload_too_large413Send a smaller body (64 KB at most)
rate_limited429Wait for the Retry-After seconds, then retry
internal_error500Retry once with backoff
provider_unavailable503Retry with backoff unless retryable is false

Error codes the API can return

Step 5: retry on 429 with the headers

Status 429 means the client has sent too many requests in a given amount of time, and the server may say how long to wait with Retry-After 4. That field holds either an HTTP-date or a number of seconds 3. Our API sends whole seconds, and the function reads them. Successful responses also carry X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (a Unix time in seconds), which let a batch job slow down before it is refused. The current limit is on the rate limits page.

const API = 'https://YOUR_API_HOST';
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
​
export async function checkEmail(email, maxAttempts = 4) {
  for (let attempt = 1; ; attempt++) {
    let res;
    try {
      res = await fetch(API + '/v1/email-domain-check', {
        method: 'POST',
        headers: {
          Authorization: 'Bearer ' + process.env.LOOKVALID_API_KEY,
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ email }),
      });
    } catch (networkError) {
      if (attempt >= maxAttempts) throw networkError;
      await sleep(500 * 2 ** attempt);
      continue;
    }
    const body = await res.json().catch(() => ({}));
    if (res.status === 200) return body;
    const err = body.error ?? {};
    if (res.status === 429 && attempt < maxAttempts) {
      await sleep((Number(res.headers.get('Retry-After')) || 1) * 1000);
      continue;
    }
    if (err.retryable && attempt < maxAttempts) {
      await sleep(500 * 2 ** attempt);
      continue;
    }
    throw Object.assign(new Error(err.message ?? 'request failed'), { code: err.code, status: res.status, requestId: err.request_id });
  }
}
​
console.log(await checkEmail('info@example.com'));

A complete function with error handling and 429 retries

Run it

  1. Save the function as an ES module, for example check.mjs.
  2. Export LOOKVALID_API_KEY in your shell.
  3. Run node check.mjs and read the found and data fields.

For the full response fields, open the email domain check product page, or start from the quickstart.

Sources

  1. MDN: Window.fetch(), read 2026-10-19
  2. Node.js documentation: globals, fetch, read 2026-10-19
  3. RFC 9110: HTTP Semantics (section 10.2.3, Retry-After), read 2026-10-19
  4. RFC 6585: Additional HTTP Status Codes (section 4, 429 Too Many Requests), read 2026-10-19