> ## Documentation Index
> Fetch the complete documentation index at: https://orbisearch.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Find Emails in Bulk From Names and Domains

> Submit a list of people (first name, last name, company domain) and OrbiSearch returns a verified email address for each one. Designed for lists of up to 10,000 rows per job.

Bulk email lookup lets you find deliverable email addresses for a list of people in a single asynchronous job. Submit a list of `(first_name, last_name, domain)` tuples; OrbiSearch processes them in the background and returns a verified result for each.

## When to use bulk lookup

* **List enrichment** — turn a CSV of names + companies into a list of reachable emails.
* **Lead research at scale** — research a target list once, instead of one address at a time.
* **Periodic refresh** — re-run lookups for a customer base after a quarterly LinkedIn refresh or trade-show export.

For looking up a single person on demand, use [single email lookup](/docs/guides/email-lookup).
For verifying a list of email addresses you already have, use [bulk verification](/docs/guides/bulk-verification).

## How bulk lookup works

<Steps>
  <Step title="Submit a job">
    Send a `POST` request to `/v1/bulk-lookup` with an `input` array of `(first_name, last_name, domain)` objects. OrbiSearch returns a `job_id` immediately and begins processing in the background. Credits are deducted upfront for the unique row count.
  </Step>

  <Step title="Poll for status">
    Use `GET /v1/bulk-lookup/{job_id}` to check the job's progress. Poll every 10–30 seconds until `status` becomes `completed`.
  </Step>

  <Step title="Retrieve results">
    Once the job is `completed`, fetch results with `GET /v1/bulk-lookup/{job_id}/results`. Results are paginated — page through them with `limit` and `offset`.
  </Step>
</Steps>

## Submitting a job

Send your list of people in the JSON body. Each row needs `first_name`, `last_name`, and `domain`.

<CodeGroup>
  ```bash cURL theme={null}
  curl --request POST \
    --url "https://api.orbisearch.com/v1/bulk-lookup" \
    --header "X-API-Key: YOUR_API_KEY" \
    --header "Content-Type: application/json" \
    --data '{
      "input": [
        {"first_name": "Jane",  "last_name": "Doe",   "domain": "acme.com"},
        {"first_name": "John",  "last_name": "Smith", "domain": "bigcorp.io"},
        {"first_name": "Priya", "last_name": "Patel", "domain": "studio.xyz"}
      ]
    }'
  ```

  ```python Python theme={null}
  import requests

  response = requests.post(
      "https://api.orbisearch.com/v1/bulk-lookup",
      json={
          "input": [
              {"first_name": "Jane",  "last_name": "Doe",   "domain": "acme.com"},
              {"first_name": "John",  "last_name": "Smith", "domain": "bigcorp.io"},
              {"first_name": "Priya", "last_name": "Patel", "domain": "studio.xyz"},
          ],
      },
      headers={"X-API-Key": "YOUR_API_KEY"},
  )
  job = response.json()
  print(job["job_id"], job["total_rows"], "credits charged:", job["credits_consumed"])
  ```

  ```javascript Node.js theme={null}
  const response = await fetch(
    "https://api.orbisearch.com/v1/bulk-lookup",
    {
      method: "POST",
      headers: {
        "X-API-Key": "YOUR_API_KEY",
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        input: [
          { first_name: "Jane",  last_name: "Doe",   domain: "acme.com" },
          { first_name: "John",  last_name: "Smith", domain: "bigcorp.io" },
          { first_name: "Priya", last_name: "Patel", domain: "studio.xyz" },
        ],
      }),
    }
  );
  const job = await response.json();
  console.log(job.job_id, job.total_rows, "credits charged:", job.credits_consumed);
  ```
</CodeGroup>

OrbiSearch responds with a job ID and the deduplicated row count:

```json theme={null}
{
  "job_id": "123e4567-e89b-12d3-a456-426614174000",
  "status": "submitted",
  "total_rows": 3,
  "credits_consumed": 3
}
```

OrbiSearch deduplicates the `input` array automatically (case-insensitive on first name + last name + domain). If your list has duplicates, you are only charged for the unique rows.

## Checking job status

Poll `GET /v1/bulk-lookup/{job_id}` to track progress.

<CodeGroup>
  ```bash cURL theme={null}
  curl --request GET \
    --url "https://api.orbisearch.com/v1/bulk-lookup/123e4567-e89b-12d3-a456-426614174000" \
    --header "X-API-Key: YOUR_API_KEY"
  ```

  ```python Python theme={null}
  status_response = requests.get(
      f"https://api.orbisearch.com/v1/bulk-lookup/{job_id}",
      headers={"X-API-Key": "YOUR_API_KEY"},
  )
  status = status_response.json()
  print(status["status"], status["total_processed"], "/", status["total_rows"])
  ```

  ```javascript Node.js theme={null}
  const statusRes = await fetch(
    `https://api.orbisearch.com/v1/bulk-lookup/${job_id}`,
    { headers: { "X-API-Key": "YOUR_API_KEY" } }
  );
  const status = await statusRes.json();
  console.log(status.status, `${status.total_processed}/${status.total_rows}`);
  ```
</CodeGroup>

While the job is running, the response looks like this:

```json theme={null}
{
  "job_id": "123e4567-e89b-12d3-a456-426614174000",
  "status": "running",
  "total_rows": 3,
  "total_processed": 2,
  "summary": {
    "total_found": 1,
    "total_not_found": 1
  },
  "created_at": "2026-04-26T06:30:00Z",
  "finished_at": null
}
```

`status` is `running` until every row has been processed, at which point it becomes `completed` and `finished_at` is populated. The `summary` object updates live as rows complete — you can show progress to your end user before the job is fully done.

## Retrieving results

Once `status` is `completed`, fetch the full results. Use `limit` and `offset` to page through them — default `limit` is 50, max 500.

<CodeGroup>
  ```bash cURL theme={null}
  curl --request GET \
    --url "https://api.orbisearch.com/v1/bulk-lookup/123e4567-e89b-12d3-a456-426614174000/results?limit=100&offset=0" \
    --header "X-API-Key: YOUR_API_KEY"
  ```

  ```python Python theme={null}
  results_response = requests.get(
      f"https://api.orbisearch.com/v1/bulk-lookup/{job_id}/results",
      params={"limit": 100, "offset": 0},
      headers={"X-API-Key": "YOUR_API_KEY"},
  )
  data = results_response.json()
  for row in data["results"]:
      if row["status"] == "safe":
          print(f"{row['first_name']} {row['last_name']} → {row['email']}")
      else:
          print(f"{row['first_name']} {row['last_name']} → not found")
  ```

  ```javascript Node.js theme={null}
  const resultsRes = await fetch(
    `https://api.orbisearch.com/v1/bulk-lookup/${job_id}/results?limit=100&offset=0`,
    { headers: { "X-API-Key": "YOUR_API_KEY" } }
  );
  const data = await resultsRes.json();
  for (const row of data.results) {
    if (row.status === "safe") {
      console.log(`${row.first_name} ${row.last_name} → ${row.email}`);
    } else {
      console.log(`${row.first_name} ${row.last_name} → not found`);
    }
  }
  ```
</CodeGroup>

Each row in `results` has the same shape as a [single email lookup](/docs/api-reference/email-lookup) response:

```json theme={null}
{
  "job_id": "123e4567-e89b-12d3-a456-426614174000",
  "total_rows": 3,
  "total_processed": 3,
  "limit": 100,
  "offset": 0,
  "results": [
    {
      "email": "jane.doe@acme.com",
      "status": "safe",
      "substatus": "deliverable",
      "explanation": "Safe to email. The mailbox exists and is deliverable.",
      "email_provider": "Google Workspace",
      "mx_record": "aspmx.l.google.com",
      "is_domain_catch_all": false,
      "is_secure_email_gateway": false,
      "is_disposable": false,
      "is_role_account": false,
      "is_free": false,
      "credits_consumed": 1,
      "first_name": "Jane",
      "last_name": "Doe",
      "domain": "acme.com"
    },
    {
      "email": null,
      "status": "unknown",
      "substatus": "no_address_found",
      "explanation": "No deliverable address was found for this person at this domain.",
      "credits_consumed": 1,
      "first_name": "John",
      "last_name": "Smith",
      "domain": "bigcorp.io"
    },
    {
      "email": "priya@studio.xyz",
      "status": "safe",
      "substatus": "deliverable",
      "explanation": "Safe to email. The mailbox exists and is deliverable.",
      "email_provider": "Google Workspace",
      "mx_record": "aspmx.l.google.com",
      "is_domain_catch_all": false,
      "is_secure_email_gateway": false,
      "is_disposable": false,
      "is_role_account": false,
      "is_free": false,
      "credits_consumed": 1,
      "first_name": "Priya",
      "last_name": "Patel",
      "domain": "studio.xyz"
    }
  ]
}
```

When `email` is `null`, OrbiSearch could not find a deliverable address for that person at that domain — don't send to them at that domain.

## Pricing

Each unique row costs `1 credit`, deducted upfront when you submit the job. Every returned row is charged. Refunds are issued only when OrbiSearch infrastructure cannot accept or process the submission itself — in that case the upfront charge is reversed and no job is created.

## Limits and rate limits

* Maximum `10,000` rows per submission. Split larger lists across multiple jobs.
* This endpoint shares the `20` requests per second per API key limit with the rest of the `/v1/*` API.

## Complete example

```python Python theme={null}
import time
import requests

API_KEY = "YOUR_API_KEY"
BASE_URL = "https://api.orbisearch.com"
HEADERS = {"X-API-Key": API_KEY, "Content-Type": "application/json"}

people = [
    {"first_name": "Jane",  "last_name": "Doe",   "domain": "acme.com"},
    {"first_name": "John",  "last_name": "Smith", "domain": "bigcorp.io"},
    {"first_name": "Priya", "last_name": "Patel", "domain": "studio.xyz"},
    # ... up to 10,000 rows
]

# 1. Submit
submit = requests.post(f"{BASE_URL}/v1/bulk-lookup", headers=HEADERS, json={"input": people})
submit.raise_for_status()
job = submit.json()
job_id = job["job_id"]
print(f"Submitted: {job_id} ({job['total_rows']} unique rows, {job['credits_consumed']} credits charged)")

# 2. Poll until complete
while True:
    status = requests.get(f"{BASE_URL}/v1/bulk-lookup/{job_id}", headers=HEADERS).json()
    print(f"  {status['status']}: {status['total_processed']}/{status['total_rows']}")
    if status["status"] == "completed":
        break
    time.sleep(15)

# 3. Page through results
offset = 0
while True:
    page = requests.get(
        f"{BASE_URL}/v1/bulk-lookup/{job_id}/results",
        params={"limit": 500, "offset": offset},
        headers=HEADERS,
    ).json()
    if not page["results"]:
        break
    for row in page["results"]:
        if row["status"] == "safe":
            print(f"{row['first_name']} {row['last_name']} → {row['email']}")
        else:
            print(f"{row['first_name']} {row['last_name']} → not found")
    offset += len(page["results"])
```
