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.
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
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.
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.
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": "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
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"])