{
  "openapi": "3.1.0",
  "info": {
    "title": "Email Finder API",
    "version": "1.0.0",
    "description": "Find the public contact email a website, social profile or YouTube channel publishes. Confidence is graded on where an address was published rather than on what it ends in, so an address the target did not publish on its own pages never grades high. A target that publishes nothing answers NO_EMAIL_LISTED — a successful answer — while a target that could not be read at all answers 503."
  },
  "servers": [
    {
      "url": "https://api.fetchlayer.dev/email-finder"
    }
  ],
  "security": [
    {
      "BearerAuth": []
    }
  ],
  "components": {
    "securitySchemes": {
      "BearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "API Key",
        "description": "Send your FetchLayer key as Authorization: Bearer ss-your-key."
      }
    }
  },
  "paths": {
    "/find-email": {
      "post": {
        "operationId": "find-email",
        "tags": [
          "Lookup"
        ],
        "summary": "Find a contact email",
        "description": "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.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "target": {
                    "type": "string",
                    "description": "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": {
                    "type": "string",
                    "description": "Envelope format: json or markdown.",
                    "example": "json"
                  }
                },
                "additionalProperties": false,
                "required": [
                  "target"
                ]
              },
              "examples": {
                "default": {
                  "value": {
                    "target": "python.org"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Successful response",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "transport": {
                      "type": "string",
                      "description": "The source. \"public-web\"."
                    },
                    "notes": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "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": {
                      "type": "number",
                      "description": "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": {
                      "type": "string",
                      "description": "The target exactly as you sent it."
                    },
                    "normalizedInput": {
                      "type": "string",
                      "description": "The target after normalisation."
                    },
                    "targetKind": {
                      "type": "string",
                      "description": "What the target was taken to be: website, social, youtube."
                    },
                    "resolvedUrl": {
                      "type": "string",
                      "description": "The page the search started from. For a YouTube channel this is its /about page."
                    },
                    "entityName": {
                      "type": "string",
                      "description": "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": {
                      "type": "string",
                      "description": "The handle, on a social or YouTube target, e.g. \"@mkbhd\"."
                    },
                    "email": {
                      "type": "string",
                      "description": "The address found, or null on a NO_EMAIL_LISTED."
                    },
                    "emailSource": {
                      "type": "string",
                      "description": "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": {
                      "type": "string",
                      "description": "The exact page the address was read off, so any grade can be checked by hand."
                    },
                    "confidence": {
                      "type": "string",
                      "description": "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": {
                      "type": "boolean",
                      "description": "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": {
                      "type": "string",
                      "description": "EMAIL_FOUND or NO_EMAIL_LISTED. Both are successful answers — the failure case is a 503."
                    },
                    "searched": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "The stages that ran, e.g. landing-page, website-pages, linked-social-profiles, web-search, and on a YouTube target recent-video-descriptions."
                    },
                    "incomplete": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "Stages that started but could not be finished. Empty on a whole answer."
                    },
                    "complete": {
                      "type": "boolean",
                      "description": "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": {
                      "type": "number",
                      "description": "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": {
                      "type": "string",
                      "description": "When the lookup ran, ISO 8601."
                    }
                  },
                  "additionalProperties": false
                },
                "examples": {
                  "default": {
                    "value": {
                      "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"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Invalid or missing Bearer token"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/resolve-target": {
      "post": {
        "operationId": "resolve-target",
        "tags": [
          "Utilities"
        ],
        "summary": "Parse a target",
        "description": "Parse a target without fetching anything, and see what a lookup would do with it: which kind it is, which page the search would start from, and which stages it would run. The right first step for a list of pasted targets — it normalises and sorts them, and shows what each would cost in effort, before a single lookup is spent. **Free.** It makes no upstream request at all, answers in about a tenth of a second, and reports `pagesFetched: 0`.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "target": {
                    "type": "string",
                    "description": "Any website, social profile URL or YouTube channel to parse."
                  },
                  "format": {
                    "type": "string",
                    "description": "Envelope format: json or markdown.",
                    "example": "json"
                  }
                },
                "additionalProperties": false,
                "required": [
                  "target"
                ]
              },
              "examples": {
                "default": {
                  "value": {
                    "target": "https://x.com/naval"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Successful response",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "transport": {
                      "type": "string",
                      "description": "The source. \"public-web\"."
                    },
                    "notes": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "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": {
                      "type": "number",
                      "description": "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": {
                      "type": "string",
                      "description": "The target exactly as you sent it."
                    },
                    "normalizedInput": {
                      "type": "string",
                      "description": "The target after normalisation."
                    },
                    "targetKind": {
                      "type": "string",
                      "description": "What the target was taken to be: website, social, youtube."
                    },
                    "resolvedUrl": {
                      "type": "string",
                      "description": "The page the search started from. For a YouTube channel this is its /about page."
                    },
                    "wouldSearch": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "The stages a lookup would run for this target, in order. A longer list is a longer lookup."
                    }
                  },
                  "additionalProperties": false
                },
                "examples": {
                  "default": {
                    "value": {
                      "transport": "public-web",
                      "notes": [],
                      "pagesFetched": 0,
                      "input": "https://x.com/naval",
                      "normalizedInput": "https://x.com/naval",
                      "targetKind": "social",
                      "resolvedUrl": "https://x.com/naval",
                      "wouldSearch": [
                        "landing-page",
                        "website-pages",
                        "linked-social-profiles",
                        "web-search"
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Invalid or missing Bearer token"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    }
  }
}