<!-- Email Finder — https://docs.warmerly.com/email-finder -->

# Email Finder

Find and verify a person's or company's email address from a domain, website, or
business name — optionally narrowed to a specific person. Lookups run asynchronously
as **jobs**: most resolve in a few seconds, but cache misses can take longer while
Warmerly scrapes and verifies candidates, so the API returns immediately with either
a completed result or a job you poll.

All Email Finder endpoints require `X-Api-Key`. Endpoints that create or read job
data are project-scoped and also require `X-Project-Id`; `GET /email-finder/quota`
is account-level and does not require `X-Project-Id`.

## Single lookup

```
POST /email-finder/lookup
```

Submit one lookup. Provide at least one of `domain`, `website`, or `name` — add
`person` to narrow the search to a specific individual at that domain/company.

| Field | Type | Description |
| --- | --- | --- |
| `domain` | string | Company domain, e.g. `"acme.com"`. |
| `website` | string | Full website URL; the domain is extracted from it. |
| `name` | string | Business name, used when no domain/website is known. |
| `location` | string | Optional location hint to disambiguate a business name. |
| `person.firstName` | string | Optional first name to target a specific person. |
| `person.lastName` | string | Optional last name to target a specific person. |

```bash
curl https://app.warmerly.com/api/v1/email-finder/lookup \
  -X POST \
  -H "X-Api-Key: wmv_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "X-Project-Id: 8f14e45f-ceea-4c6a-8c8d-6b1a5b5c9c1a" \
  -H "Content-Type: application/json" \
  -d '{
    "domain": "acme.com",
    "person": { "firstName": "Jane", "lastName": "Doe" }
  }'
```

If the answer is already cached, the lookup completes synchronously and returns
`200` with the result inline:

```json
{
  "status": "done",
  "jobId": "3f9a2e2a-1c4b-4a2e-9b1e-2f6c8a7d5e10",
  "job": {
    "id": "3f9a2e2a-1c4b-4a2e-9b1e-2f6c8a7d5e10",
    "status": "done",
    "confidence": 92,
    "error": null,
    "createdAt": "2026-07-02T09:14:03.000Z",
    "completedAt": "2026-07-02T09:14:03.000Z"
  },
  "result": {
    "email": "jane.doe@acme.com",
    "confidence": 92,
    "verifyStatus": "verified",
    "source": "pattern_guess",
    "isCatchAll": false,
    "mxHost": "aspmx.l.google.com",
    "evidenceUrl": "https://acme.com/team",
    "providerName": "Google Workspace"
  }
}
```

Otherwise it enqueues a job and returns `202` with a `pollUrl`:

```json
{
  "status": "pending",
  "jobId": "3f9a2e2a-1c4b-4a2e-9b1e-2f6c8a7d5e10",
  "pollUrl": "https://app.warmerly.com/api/v1/email-finder/jobs/3f9a2e2a-1c4b-4a2e-9b1e-2f6c8a7d5e10"
}
```

Poll the job (see below) until `status` is `"done"` or `"failed"`.

## Bulk lookup

```
POST /email-finder/bulk
```

Submit up to 1,000 lookups in one request. Each item accepts the same fields as a
single lookup. All items are enqueued under a shared `batchId` you can use to track
progress via [Batches](#batch-status).

```bash
curl https://app.warmerly.com/api/v1/email-finder/bulk \
  -X POST \
  -H "X-Api-Key: wmv_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "X-Project-Id: 8f14e45f-ceea-4c6a-8c8d-6b1a5b5c9c1a" \
  -H "Content-Type: application/json" \
  -d '{
    "items": [
      { "domain": "acme.com", "person": { "firstName": "Jane", "lastName": "Doe" } },
      { "website": "https://widgetco.io" },
      { "name": "Roasted Coffee Co", "location": "Austin, TX" }
    ]
  }'
```

```json
{
  "batchId": "6e6f9d0a-4a3f-4c7e-9c86-1b6a4e5c2f01",
  "results": [
    { "index": 0, "ok": true, "jobId": "3f9a2e2a-1c4b-4a2e-9b1e-2f6c8a7d5e10", "cached": true, "result": { "email": "jane.doe@acme.com", "confidence": 92, "verifyStatus": "verified", "source": "pattern_guess", "isCatchAll": false, "mxHost": "aspmx.l.google.com", "evidenceUrl": "https://acme.com/team", "providerName": "Google Workspace" } },
    { "index": 1, "ok": true, "jobId": "7c1e9b3d-8a2f-4e11-9d0c-5b3a1f4e6c22", "cached": false },
    { "index": 2, "ok": false, "error": "invalid input" }
  ]
}
```

Each item is evaluated independently — an invalid item (`ok: false`) does not fail
the rest of the batch. Items that hit the cache resolve immediately (`cached: true`,
`result` present); the rest are enqueued (`cached: false`) and must be polled
individually via their `jobId`, or tracked in aggregate via batch status.

## Get a job

```
GET /email-finder/jobs/:id
```

Fetch the current status of a lookup job, whether it came from a single or bulk
request.

```bash
curl https://app.warmerly.com/api/v1/email-finder/jobs/3f9a2e2a-1c4b-4a2e-9b1e-2f6c8a7d5e10 \
  -H "X-Api-Key: wmv_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "X-Project-Id: 8f14e45f-ceea-4c6a-8c8d-6b1a5b5c9c1a"
```

```json
{
  "job": {
    "id": "3f9a2e2a-1c4b-4a2e-9b1e-2f6c8a7d5e10",
    "status": "scraping",
    "confidence": null,
    "error": null,
    "createdAt": "2026-07-02T09:14:03.000Z",
    "completedAt": null
  },
  "result": null
}
```

`status` moves through `pending` → `resolving` → `scraping` → `verifying` →
`done` (or `failed`). `result` is populated once `status` is `"done"`; on
`"failed"`, check `job.error` for the reason. A job for a project you don't have
access to (or that doesn't exist) returns `404`.

## Delete a job

```
DELETE /email-finder/jobs/:id
```

Deletes the job and its discovered email records.

```bash
curl https://app.warmerly.com/api/v1/email-finder/jobs/3f9a2e2a-1c4b-4a2e-9b1e-2f6c8a7d5e10 \
  -X DELETE \
  -H "X-Api-Key: wmv_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "X-Project-Id: 8f14e45f-ceea-4c6a-8c8d-6b1a5b5c9c1a"
```

```json
{ "deleted": "3f9a2e2a-1c4b-4a2e-9b1e-2f6c8a7d5e10" }
```

## Batch status

```
GET /email-finder/batches/:id
```

Returns aggregate progress for a bulk lookup, grouped by job status.

```bash
curl https://app.warmerly.com/api/v1/email-finder/batches/6e6f9d0a-4a3f-4c7e-9c86-1b6a4e5c2f01 \
  -H "X-Api-Key: wmv_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "X-Project-Id: 8f14e45f-ceea-4c6a-8c8d-6b1a5b5c9c1a"
```

```json
{
  "batchId": "6e6f9d0a-4a3f-4c7e-9c86-1b6a4e5c2f01",
  "total": 3,
  "done": 2,
  "pending": 1,
  "counts": {
    "done": 2,
    "scraping": 1
  }
}
```

`done` sums jobs in either `done` or `failed` status. Use [List results](#list-results)
with `batchId` to fetch the actual emails once a batch is complete.

## List results

```
GET /email-finder/results
```

Lists jobs for the current project, each joined with its winning discovered email
(if any), newest first. Cursor-paginated.

| Query param | Type | Description |
| --- | --- | --- |
| `status` | string | Filter by job status: `pending`, `resolving`, `scraping`, `verifying`, `done`, `failed`. |
| `batchId` | uuid | Restrict to jobs from a specific bulk batch. |
| `minConfidence` | integer (0–100) | Only include results with confidence at or above this value. |
| `cursor` | ISO 8601 datetime | Pass the previous response's `nextCursor` to fetch the next page. |
| `limit` | integer (1–100, default 25) | Page size. |

```bash
curl "https://app.warmerly.com/api/v1/email-finder/results?status=done&limit=25" \
  -H "X-Api-Key: wmv_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "X-Project-Id: 8f14e45f-ceea-4c6a-8c8d-6b1a5b5c9c1a"
```

```json
{
  "results": [
    {
      "id": "3f9a2e2a-1c4b-4a2e-9b1e-2f6c8a7d5e10",
      "status": "done",
      "confidence": 92,
      "inputDomain": "acme.com",
      "inputName": null,
      "batchId": null,
      "createdAt": "2026-07-02T09:14:03.000Z",
      "email": "jane.doe@acme.com",
      "verifyStatus": "verified",
      "source": "pattern_guess"
    }
  ],
  "nextCursor": "2026-07-02T09:14:03.000Z"
}
```

Pass `nextCursor` as the `cursor` query param to fetch the next page; it is `null`
on the last page.

## Quota

```
GET /email-finder/quota
```

Returns your current monthly Email Finder usage. This endpoint is account-level —
it does not require `X-Project-Id`.

```bash
curl https://app.warmerly.com/api/v1/email-finder/quota \
  -H "X-Api-Key: wmv_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
```

```json
{ "used": 37, "limit": 500, "remaining": 463 }
```

Free-trial workspaces default to 50 lookups/month; paid plans raise the limit.
Both `POST /email-finder/lookup` and `POST /email-finder/bulk` check remaining
quota before enqueuing — a bulk request counts against quota by item count. If
the request would exceed your limit, it's rejected with `400`:

```json
{
  "error": {
    "code": "bad_request",
    "message": "plan_limit_reached",
    "details": {
      "allowed": false,
      "used": 500,
      "limit": 500,
      "remaining": 0,
      "plan": "growth"
    }
  }
}
```

## Errors

| Status | Code | When |
| --- | --- | --- |
| `400` | `bad_request` | Missing `X-Project-Id` (project-scoped endpoints), invalid body/query, no valid `domain`/`website`/`name` provided, or quota exhausted (`*_limit_reached`). |
| `401` | `unauthorized` | Missing, invalid, or revoked API key. |
| `404` | `not_found` | Job or batch doesn't exist, or doesn't belong to the given project. |
| `429` | `too_many_requests` | Rate limit exceeded. |
