---
title: "Verify email domains in Node.js, step by step"
url: "https://lookvalid.com/blog/tutorial/nodejs-email-domain-verification.md"
canonical_url: "https://lookvalid.com/blog/tutorial/nodejs-email-domain-verification"
last_updated: "2026-10-22"
type: "how-to"
summary: "Call the email domain check from Node.js with fetch, handle found false, read the error envelope and retry on 429 using the rate limit headers."
related: ["https://lookvalid.com/products/email-domain-check.md", "https://lookvalid.com/docs/errors.md", "https://lookvalid.com/docs/rate-limits.md", "https://lookvalid.com/docs/quickstart.md"]
---

# Verify email domains in Node.js, step by step

Call the email domain check from Node.js with fetch, handle found false, read the error envelope and retry on 429 using the rate limit headers.

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.

## 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.

The request as curl, to test your key first

```bash
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"}'
```

## 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](https://lookvalid.com/docs/errors.md).

Error codes the API can return

| Code | HTTP | What to do |
| --- | --- | --- |
| invalid_input | 400 | Fix the request; retrying as sent will not help |
| signature_invalid | 400 | Fix the signature; not retryable as sent |
| unauthorized | 401 | Send a valid key |
| insufficient_credits | 402 | Add credits, then retry |
| forbidden | 403 | The key may not do this; do not retry |
| unknown_product, not_found | 404 | Check the product id or the path |
| method_not_allowed | 405 | Use POST |
| conflict | 409 | Do not retry as sent |
| payload_too_large | 413 | Send a smaller body (64 KB at most) |
| rate_limited | 429 | Wait for the Retry-After seconds, then retry |
| internal_error | 500 | Retry once with backoff |
| provider_unavailable | 503 | Retry with backoff unless `retryable` is false |

## 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](https://lookvalid.com/docs/rate-limits.md) page.

A complete function with error handling and 429 retries

```js
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'));
```

## 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](https://lookvalid.com/products/email-domain-check.md), or start from the [quickstart](https://lookvalid.com/docs/quickstart.md).

## Sources

1. [MDN: Window.fetch()](https://developer.mozilla.org/en-US/docs/Web/API/Window/fetch), read 2026-10-19
2. [Node.js documentation: globals, fetch](https://nodejs.org/api/globals.html), read 2026-10-19
3. [RFC 9110: HTTP Semantics (section 10.2.3, Retry-After)](https://www.rfc-editor.org/rfc/rfc9110.html), read 2026-10-19
4. [RFC 6585: Additional HTTP Status Codes (section 4, 429 Too Many Requests)](https://www.rfc-editor.org/rfc/rfc6585.html), read 2026-10-19

By LookValid engineering. Published 2026-10-19, updated 2026-10-19.

## Related

- [Email Domain Check API](https://lookvalid.com/products/email-domain-check.md)
- [Errors](https://lookvalid.com/docs/errors.md)
- [Rate limits](https://lookvalid.com/docs/rate-limits.md)
- [Quickstart](https://lookvalid.com/docs/quickstart.md)
