Integration Guide
How to Find a Business Email Without a Paid Lead Database
'How do you find email addresses from LinkedIn profiles these days?' is a live thread with 49 comments and no good answer. Here's an automatable version that only ever returns addresses a target actually published.
Written by Alex P.
- email finder
- lead enrichment
- cold email
- Hunter.io alternative
- contact discovery
“How are you finding leads without paying for expensive tools like Apollo or Hunter.io?” and “How do you find email addresses from LinkedIn profiles these days?” are both live, active questions with dozens of replies and no real consensus — because most of the advice is the same manual routine: find the person’s employer, guess the company’s email pattern from one known address, then run a handful of Google dorks (site:acme.com "Jane Smith") and hope something surfaces.
That workflow is automatable, and the automated version can do something the pattern-guessing approach can’t: tell you, per result, whether the address it found was actually published by the target or just guessed at — which matters a lot once you’re sending mail off the back of it.
What this does differently: no guessing
Most email finders work by guessing a pattern (firstname.lastname@company.com) from one confirmed address, then verifying it with an SMTP check. That produces a plausible-looking address with no proof it’s real. find-email does the opposite: it only ever returns an address that was actually published somewhere — on the target’s own site, a linked social profile, or a page that names them — and tells you exactly which:
const API_KEY = process.env.FETCHLAYER_API_KEY;
async function findEmail(target) {
const res = await fetch('https://api.fetchlayer.dev/email-finder/find-email', {
method: 'POST',
headers: { Authorization: `Bearer ${API_KEY}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ target }),
});
return res.json();
}
const result = await findEmail('acme.com');
console.log(result.status); // EMAIL_FOUND or NO_EMAIL_LISTED — both are successful answers
console.log(result.email);
console.log(result.confidence); // high, medium, or low
console.log(result.emailSource); // "website" or "web-search" — this is what caps confidence
console.log(result.sourceUrl); // the exact page it was read from, so you can check it by hand
target takes a website, a social profile URL, or a YouTube channel — a bare domain works fine, and for a social or video target, the search runs against that profile’s own published information, not a guess based on the platform.
Reading confidence correctly
confidence is gated on where the address came from, not on how it looks. An address found on the target’s own pages (emailSource: "website") can reach high. An address found via emailSource: "web-search" — a page elsewhere that happens to name the target — tops out lower, because it’s one more hop removed from the target actually publishing it themselves:
function isSafeToUse(result) {
if (result.status !== 'EMAIL_FOUND') return false;
return result.confidence === 'high' || result.confidence === 'medium';
}
Don’t discard a low-confidence result outright, though — check onTargetDomain too. A creator’s YouTube channel target with an address found on their personal website reads onTargetDomain: false (the host doesn’t match youtube.com) and gets graded low, even though sourceUrl shows it came straight from a page they own. low here means “off the target’s home platform,” not “unreliable” — read sourceUrl yourself before writing the result off.
Processing a list
This is a single-target lookup — there’s no batch endpoint — so a list of companies runs sequentially, one lookup per target:
async function findEmailsForList(targets) {
const results = [];
for (const target of targets) {
try {
const result = await findEmail(target);
results.push({ target, ...result });
} catch (e) {
results.push({ target, status: 'ERROR', error: String(e) });
}
}
return results;
}
const leads = ['acme.com', 'example.org', 'https://x.com/naval'];
const found = await findEmailsForList(leads);
const usable = found.filter((r) => r.status === 'EMAIL_FOUND' && r.confidence !== 'low');
console.log(`${usable.length} of ${found.length} targets have a usable address.`);
NO_EMAIL_LISTED is a normal, complete answer, not an error to retry — it means the search ran fully and the target simply doesn’t publish an address anywhere it checked. It’s still billed as one completed lookup, same as a hit, so budget for the fact that not every target in a list will resolve.
Checking whether the search actually finished
A lookup can complete with some stages unfinished — a linked page that timed out, for instance — and the response is explicit about it rather than silently returning a partial answer as if it were whole:
function wasFullSearch(result) {
// Read `complete` directly rather than inferring wholeness from how long
// the request took — a fast NO_EMAIL_LISTED and a fast partial failure
// look identical from the outside.
if (!result.complete) {
console.warn(`Incomplete search for target — missed: ${result.incomplete.join(', ')}`);
}
return result.complete;
}
searched lists which stages actually ran (landing-page, website-pages, linked-social-profiles, web-search, and recent-video-descriptions for a YouTube target) — useful context for understanding why a particular result came back low confidence or empty.
A practical enrichment pipeline
import { readFile, writeFile } from 'node:fs/promises';
async function enrichLeadList(companiesFile, outputFile) {
const companies = JSON.parse(await readFile(companiesFile, 'utf8'));
const enriched = [];
for (const company of companies) {
const result = await findEmail(company.domain);
enriched.push({
...company,
email: result.status === 'EMAIL_FOUND' ? result.email : null,
confidence: result.confidence,
source: result.sourceUrl,
});
}
await writeFile(outputFile, JSON.stringify(enriched, null, 2));
const found = enriched.filter((c) => c.email).length;
console.log(`Found ${found}/${companies.length} addresses.`);
}
Practical notes
resolve-targetis free and doesn’t cost a lookup. If you just need to confirm what a target normalizes to (targetKind,entityName) before committing to a full search across a large list, check there first — it bills atpagesFetched: 0.- A bare handle isn’t enough for a social target —
find-emailneeds the full profile address (https://x.com/naval), not justnaval, since a handle alone isn’t unique across platforms. pagesCrawledis transparency, not billing. It can range from 1 to 65 pages read during one search; what you’re actually charged is alwayspagesFetched: 1per completed lookup, regardless of how many pages that lookup read internally.
Next Steps
- Email Finder API — full endpoint reference and pricing
- Get a free API key — no credit card required