Email Finder API — Find a Public Contact Address
Documentation menu
FetchLayer FetchLayer API

Find a contact email

Find the public contact email a target publishes. `target` is a website (`python.org`), a social profile URL (`https://x.com/naval`) or a YouTube channel (`https://www.youtube.com/@mkbhd`). **`NO_EMAIL_LISTED` is a successful answer, not a failure.** It means the search ran and the target publishes no readable address — and that is the common case for big, well-run companies, not the exception: github.com, g2.com and ticketmaster.com all answer this way, each with a note naming every stage that was checked. Stripe publishes a support portal rather than an address. The failure case has its own status: a target that could not be read at all is a **503** — indeed.com returned one in 6.2s, and a domain that does not resolve in 2.8s, both with `{"error":"the target could not be read right now, please retry"}`. That separation is what makes a "no" worth trusting: retry the 503, and take the `NO_EMAIL_LISTED` as the answer it is. **`confidence` is gated on provenance, not on the domain.** An address the target did not publish on its own pages can never grade `high`, however right the domain looks. Measured: `python.org` returned `psf@python.org` from the target's own /about page at **high**; `stripe.com` returned an address that really is `@stripe.com`, found on a Play Store listing, at **medium** — on the right domain and still not high, because Stripe did not publish it there. The rule exists because a lookup once returned `jane.diaz@stripe.com` at high confidence: Stripe's own fictional demo persona from their checkout mock-ups, a plausible address on a perfect domain belonging to nobody. Grading on where an address was published rather than on what it ends in is what stops that class of answer. Read `emailSource`, `sourceUrl` and `onTargetDomain` alongside the grade rather than the grade alone. **The response says what it looked at.** `searched[]` lists the stages that ran, `incomplete[]` lists any that could not finish, and `complete` summarises the two — so a partial answer is always distinguishable from a failed one, and from a whole one. **Which inputs are strong:** websites and YouTube channels. Every website and YouTube target measured returned a complete answer. Social lookups are supported, can take longer, and may return a partial result, which the response marks as incomplete — the one partial observed was `https://x.com/paulg` at 95.8s, with `complete: false` and `incomplete: ["web-search"]`. **Latency** is driven by how far a target pushes the search rather than by the kind of target alone, so it does not sort neatly by input type: 9.5s to 101.6s measured for a 200 (websites 9.5-101.6s, YouTube 50.9-77.6s, social 19.6-95.8s). Budget timeouts against the top of that range. **Billing: one credit per completed lookup, including a `NO_EMAIL_LISTED`.** The same work runs either way — the same pages are fetched, the same stages run — and what you are paying for is the search, not the address. `pagesCrawled` (1 to 65 across the set measured) is there so the size of a search is visible; it prices nothing.

POST /find-email Lookup
https://api.fetchlayer.dev/email-finder/find-email
OpenAPI JSON

Headers

Authorization string required

Bearer token — your FetchLayer API key.

Parameters

target string required

A website (python.org), a social profile URL (https://x.com/naval) or a YouTube channel (https://www.youtube.com/@mkbhd). A bare domain is fine.

format string optional

Envelope format: json or markdown. (default: json)

Response Fields

find-email

Find a contact email

transport string

The source. "public-web".

notes string[]

Caveats you can act on, in plain language: which stages were checked and found nothing, that an address came from somewhere other than the target's own pages, or that a stage could not be finished in the time available.

pagesFetched number

What the request is billed at: 1 on every completed lookup, including a NO_EMAIL_LISTED; 0 on /resolve-target. It is a billing figure, not a crawl size — that is `pagesCrawled`.

input string

The target exactly as you sent it.

normalizedInput string

The target after normalisation.

targetKind string

What the target was taken to be: website, social, youtube.

resolvedUrl string

The page the search started from. For a YouTube channel this is its /about page.

entityName string

The name of whatever the target turned out to be, where one could be read. Null on an X profile, which publishes none to this reader.

entityHandle string

The handle, on a social or YouTube target, e.g. "@mkbhd".

email string

The address found, or null on a NO_EMAIL_LISTED.

emailSource string

Where it was found: "website" (the target's own pages) or "web-search" (a page elsewhere naming the target). This, not the domain, is what caps `confidence`.

sourceUrl string

The exact page the address was read off, so any grade can be checked by hand.

confidence string

high, medium, low, or null on a NO_EMAIL_LISTED. Gated on provenance: only an address the target published on its own pages can reach "high".

onTargetDomain boolean

Whether the address sits on the target's own host. Measured against the target as given: for a YouTube channel the host is youtube.com, so an address published on the creator's own website reads false — `@veritasium` returned `veritasium@gmail.com` found on veritasium.com with `onTargetDomain: false` and a `low` grade. That is correct behaviour; `sourceUrl` is what tells you it is still a good address.

status string

EMAIL_FOUND or NO_EMAIL_LISTED. Both are successful answers — the failure case is a 503.

searched string[]

The stages that ran, e.g. landing-page, website-pages, linked-social-profiles, web-search, and on a YouTube target recent-video-descriptions.

incomplete string[]

Stages that started but could not be finished. Empty on a whole answer.

complete boolean

The one-field summary of the two lists above. **Read this rather than inferring wholeness from the elapsed time.** False means some of the search did not run, and the note says which and why.

pagesCrawled number

How many pages the search actually read — 1 to 65 across the set measured. Transparency only: it prices nothing, and `pagesFetched` is the billing figure.

scrapedAt string

When the lookup ran, ISO 8601.

Request

curl -X POST "https://api.fetchlayer.dev/email-finder/find-email" \
  -H "Authorization: Bearer ss-your-key" \
  -H "Content-Type: application/json" \
  -d '{"target":"python.org"}'

Response

{
  "transport": "public-web",
  "notes": [],
  "pagesFetched": 1,
  "input": "python.org",
  "normalizedInput": "python.org",
  "targetKind": "website",
  "resolvedUrl": "https://www.python.org/",
  "entityName": "Welcome to Python.org",
  "entityHandle": null,
  "email": "psf@python.org",
  "emailSource": "website",
  "sourceUrl": "https://www.python.org/about",
  "confidence": "high",
  "onTargetDomain": true,
  "status": "EMAIL_FOUND",
  "searched": [
    "landing-page",
    "linked-social-profiles",
    "website-pages"
  ],
  "incomplete": [],
  "complete": true,
  "pagesCrawled": 16,
  "scrapedAt": "2026-09-16T22:54:40.477Z"
}