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

# Enrich Companies in Bulk From LinkedIn URLs or Domains

> Submit a list of LinkedIn company URLs or website domains and get a company record back for each row, in the order you sent them. Up to 100,000 rows per job.

Bulk company enrichment turns a list of LinkedIn company URLs or website domains into company records in a single asynchronous job. Submit the list; OrbiSearch processes it in the background and returns one result row per submitted row, in your order, with the data from [single company enrichment](https://orbisearch.com/docs/guides/company-enrichment.md) added.

This endpoint is credit-metered and needs a pay-as-you-go API key.

## When to use bulk company enrichment

* **Account list enrichment**: add domain, industry, headcount, and headquarters to a CRM export or a list of target accounts.
* **Finding LinkedIn pages for a list of websites**: submit domains and get each company's LinkedIn URL back with its record.
* **Preparing an email finder run**: get each company's `email_domain`, then find people's work emails there by name with the [bulk email finder](https://orbisearch.com/docs/guides/bulk-lookup.md).
* **Periodic refresh**: re-run an account list to pick up changes in headcount, followers, or headquarters.

For a single company on demand, use [single company enrichment](https://orbisearch.com/docs/guides/company-enrichment.md).

## How bulk company enrichment works

**1. Submit a job**

Send a `POST` request to `/v1/company-enrichment/bulk` with an `input` array of `{"linkedin_url": ...}` or `{"domain": ...}` objects. OrbiSearch returns a `job_id` immediately and begins processing in the background. Credits are deducted upfront, once per unique input.

**2. Poll for status**

Use `GET /v1/company-enrichment/bulk/{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/company-enrichment/bulk/{job_id}/results`. Results are paginated. Page through them with `limit` and `offset`.

## Submitting a job

Send your list in the JSON body. Each row has a `linkedin_url` or a `domain`, and one job can mix both.

> [!TIP]
>
> **Use the LinkedIn URL whenever you have it.** It names the company directly, so it gives the best results. Use `domain` for rows where you only have the website. If a row fills both, the LinkedIn URL is used.

A domain can be bare (`kearney.com`), a subdomain or a full URL, and only returns a company whose own website is on that domain. See [searching by domain](https://orbisearch.com/docs/guides/company-enrichment.md#searching-by-domain) for how domains are matched and how redirects are reported.

```bash cURL theme={null}
curl --request POST \
  --url "https://api.orbisearch.com/v1/company-enrichment/bulk" \
  --header "X-API-Key: YOUR_API_KEY" \
  --header "Content-Type: application/json" \
  --data '{
    "input": [
      {"linkedin_url": "linkedin.com/company/kearney"},
      {"linkedin_url": "https://uk.linkedin.com/company/kearney/about/"},
      {"domain": "oliverwyman.com"},
      {"linkedin_url": "linkedin.com/company/acme-widgets-intl"},
      {"linkedin_url": "https://www.linkedin.com/company/1035"}
    ]
  }'
```

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

response = requests.post(
    "https://api.orbisearch.com/v1/company-enrichment/bulk",
    json={
        "input": [
            {"linkedin_url": "linkedin.com/company/kearney"},
            {"linkedin_url": "https://uk.linkedin.com/company/kearney/about/"},
            {"domain": "oliverwyman.com"},
            {"linkedin_url": "linkedin.com/company/acme-widgets-intl"},
            {"linkedin_url": "https://www.linkedin.com/company/1035"},
        ],
    },
    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/company-enrichment/bulk",
  {
    method: "POST",
    headers: {
      "X-API-Key": "YOUR_API_KEY",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      input: [
        { linkedin_url: "linkedin.com/company/kearney" },
        { linkedin_url: "https://uk.linkedin.com/company/kearney/about/" },
        { domain: "oliverwyman.com" },
        { linkedin_url: "linkedin.com/company/acme-widgets-intl" },
        { linkedin_url: "https://www.linkedin.com/company/1035" },
      ],
    }),
  }
);
const job = await response.json();
console.log(job.job_id, job.total_rows, "credits charged:", job.credits_consumed);
```

OrbiSearch responds with a job ID, the row counts, and the credits charged:

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

In this example, the first two rows are the same company written two ways, so they are charged once. The numeric company URL in the last row isn't supported, so it is skipped and costs nothing. That leaves three unique inputs and `3` credits.

### Duplicates and invalid rows

* **Duplicates are charged once**, even when they are written differently. Each duplicate still gets its own row in the results, with the same data. A LinkedIn URL and a domain for the same company are two inputs, so each is charged. Send one or the other per company.
* **Invalid rows don't fail the job.** A row that isn't a usable LinkedIn company URL (a person's profile, a numeric company URL, a blank value, or a company name over 200 characters) or website domain (a LinkedIn URL in the `domain` field, or a value that isn't a domain) is skipped, costs nothing, and comes back as `result: invalid_url` with a `reason`. The request is rejected with `422` only when no valid row is left.

## Checking job status

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

```bash cURL theme={null}
curl --request GET \
  --url "https://api.orbisearch.com/v1/company-enrichment/bulk/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/company-enrichment/bulk/{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/company-enrichment/bulk/${job_id}`,
  { headers: { "X-API-Key": "YOUR_API_KEY" } }
);
const status = await statusRes.json();
console.log(status.status, `${status.total_processed}/${status.total_rows}`);
```

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

```json theme={null}
{
  "job_id": "123e4567-e89b-12d3-a456-426614174000",
  "status": "running",
  "total_rows": 5,
  "total_processed": 2,
  "credits_consumed": 3,
  "summary": {
    "total_found": 0,
    "total_not_found": 1,
    "total_error": 0,
    "total_invalid_url": 1
  },
  "created_at": "2026-09-25T09:00:00Z",
  "finished_at": null
}
```

`status` is `running` until every row has been answered, at which point it becomes `completed` and `finished_at` is populated. Even a small job can take up to a minute.

## Retrieving results

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

```bash cURL theme={null}
curl --request GET \
  --url "https://api.orbisearch.com/v1/company-enrichment/bulk/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/company-enrichment/bulk/{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["result"] == "found":
        print(f"{row['input']} → {row['name']} ({row['domain']})")
    else:
        print(f"{row['input']} → {row['result']}")
```

```javascript Node.js theme={null}
const resultsRes = await fetch(
  `https://api.orbisearch.com/v1/company-enrichment/bulk/${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.result === "found") {
    console.log(`${row.input} → ${row.name} (${row.domain})`);
  } else {
    console.log(`${row.input} → ${row.result}`);
  }
}
```

The results hold one row for every row you submitted, in the same order. Each row echoes the LinkedIn URL or domain you sent in `input` and carries the same data fields as a [single company enrichment](https://orbisearch.com/docs/guides/company-enrichment.md) response. For the job above:

| Position | `input` | `result` | `credits_consumed` |
| - | - | - | - |
| 0 | `linkedin.com/company/kearney` | `found` | `1` |
| 1 | `https://uk.linkedin.com/company/kearney/about/` | `found` (same data as row 0) | `0` |
| 2 | `oliverwyman.com` | `found`, with the company's `linkedin_url` | `1` |
| 3 | `linkedin.com/company/acme-widgets-intl` | `not_found` | `1` |
| 4 | `https://www.linkedin.com/company/1035` | `invalid_url`, with a `reason` | `0` |

See [bulk results](https://orbisearch.com/docs/api-reference/company-enrichment-bulk-results.md) for the full row shape and an example response.

A row with `result: error` is one OrbiSearch couldn't answer. It is charged like any other accepted row. You can submit those rows again in a new job.

## Pricing

Each unique input costs `1 credit`, deducted upfront when you submit the job. Duplicates and invalid rows cost nothing. Every accepted row is charged, including rows that end as `not_found` or `error`. Refunds are issued only when OrbiSearch infrastructure cannot accept the submission itself. In that case the upfront charge is reversed and no job is created.

## Limits and rate limits

* Maximum `100,000` rows per submission. Split larger lists across multiple jobs.
* This endpoint shares the `20` requests per second per account 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"}

companies = [
    {"linkedin_url": "linkedin.com/company/kearney"},
    {"linkedin_url": "https://www.linkedin.com/company/oliver-wyman"},
    {"domain": "bain.com"},  # use a domain only where you have no LinkedIn URL
    # ... up to 100,000 rows
]

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

# 2. Poll until complete
while True:
    status = requests.get(f"{BASE_URL}/v1/company-enrichment/bulk/{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, in the order you submitted them
offset = 0
while offset < status["total_rows"]:
    page = requests.get(
        f"{BASE_URL}/v1/company-enrichment/bulk/{job_id}/results",
        params={"limit": 500, "offset": offset},
        headers=HEADERS,
    ).json()
    for row in page["results"]:
        if row["result"] == "found":
            print(f"{row['input']} → {row['name']} ({row['domain']})")
        else:
            print(f"{row['input']} → {row['result']}")
    offset += len(page["results"])
```


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.