Skip to main content
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. For verifying a list of email addresses you already have, use bulk verification.

How bulk lookup works

1

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.
2

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.
3

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.

Submitting a job

Send your list of people in the JSON body. Each row needs first_name, last_name, and domain.
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"}
    ]
  }'
OrbiSearch responds with a job ID and the deduplicated row count:
{
  "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.
curl --request GET \
  --url "https://api.orbisearch.com/v1/bulk-lookup/123e4567-e89b-12d3-a456-426614174000" \
  --header "X-API-Key: YOUR_API_KEY"
While the job is running, the response looks like this:
{
  "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.
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"
Each row in results has the same shape as a single email lookup response:
{
  "job_id": "123e4567-e89b-12d3-a456-426614174000",
  "total_rows": 3,
  "total_processed": 3,
  "limit": 100,
  "offset": 0,
  "results": [
    {
      "email": "[email protected]",
      "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": "[email protected]",
      "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
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"])