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.
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.
| 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 |
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
- Save the function as an ES module, for example
check.mjs. - Export
LOOKVALID_API_KEYin your shell. - Run
node check.mjsand read thefoundanddatafields.
For the full response fields, open the email domain check product page, or start from the quickstart.
Sources
- MDN: Window.fetch(), read 2026-10-19
- Node.js documentation: globals, fetch, read 2026-10-19
- RFC 9110: HTTP Semantics (section 10.2.3, Retry-After), read 2026-10-19
- RFC 6585: Additional HTTP Status Codes (section 4, 429 Too Many Requests), read 2026-10-19