> ## 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 Verification Guide

> Submit thousands of email addresses for verification in a single job. OrbiSearch processes them asynchronously and retries greylisted addresses automatically.

Bulk verification is designed for processing large lists of email addresses in a single operation. Instead of making one request per address, you submit an array of emails and OrbiSearch handles the verification asynchronously — including automatic retries for addresses that return transient errors or are greylisted. You can then poll for status and retrieve the results when the job completes.

## When to use bulk verification

* **List cleaning** — verify an existing database of contacts before a campaign or migration.
* **Batch imports** — validate addresses in bulk when importing data from a CSV, CRM export, or third-party source.
* **Scheduled enrichment** — run periodic verification jobs to keep your data fresh without blocking real-time workflows.

For checking a single address on demand (e.g., at point of entry) or in tools like Clay, use [single verification](/docs/guides/single-verification) instead.

## How bulk verification works

<Steps>
  <Step title="Submit a job">
    Send a `POST` request to `/v1/bulk` with a JSON body containing an `emails` array. OrbiSearch returns a `job_id` immediately and begins processing in the background.
  </Step>

  <Step title="Poll for status">
    Use `GET /v1/bulk/{job_id}` to check the job's progress. Poll every 10–30 seconds for large jobs until the `status` is `complete` or `failed`.
  </Step>

  <Step title="Retrieve results">
    Once the job is complete, fetch all results with `GET /v1/bulk/{job_id}/results`. Each result contains the same fields as a single verification response.
  </Step>
</Steps>

## Submitting a job

Send your list of addresses as a JSON array in the request body.

```bash cURL theme={null}
curl --request POST \
  --url "https://api.orbisearch.com/v1/bulk" \
  --header "X-API-Key: YOUR_API_KEY" \
  --header "Content-Type: application/json" \
  --data '["jane.doe@acme.com", "info@example.com", "user@domain.org"]'
```

OrbiSearch returns a confirmation object with the job ID and an estimated cost:

```json theme={null}
{
  "job_id": "job_abc123",
  "status": "submitted",
  "total_emails": 500,
  "estimated_cost": 100.0
}
```

<Note>
  OrbiSearch deduplicates the `emails` array automatically. You are only charged for unique addresses. If your list contains duplicates, they are removed before processing and the cost reflects the unique count.
</Note>

## Checking job status

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

```bash cURL theme={null}
curl --request GET \
  --url "https://api.orbisearch.com/v1/bulk/job_abc123" \
  --header "X-API-Key: YOUR_API_KEY"
```

Example response while the job is running:

```json theme={null}
{
  "job_id": "job_abc123",
  "status": "in_progress",
  "total_emails": 500,
  "emails_processed": 243,
  "retry_status": "pending",
  "submitted_at": "2024-01-15T10:00:00Z",
  "completed_at": null
}
```

### Job statuses

| Status                      | Description                                                         |
| --------------------------- | ------------------------------------------------------------------- |
| `pending`                   | The job has been received but has not started processing yet.       |
| `in_progress`               | OrbiSearch is actively verifying addresses.                         |
| `partial_complete_retrying` | The main pass is done; OrbiSearch is retrying greylisted addresses. |
| `complete`                  | All verifications are finished. Results are ready to retrieve.      |
| `failed`                    | The job failed. Contact support if this is unexpected.              |

### Retry statuses

Some addresses return a `greylisted` substatus on the first attempt. OrbiSearch automatically retries these. The `retry_status` field tracks that process:

| `retry_status` | Description                     |
| -------------- | ------------------------------- |
| `none`         | No addresses required retrying. |
| `pending`      | Retry pass has not started yet. |
| `partial`      | Retry pass is in progress.      |
| `complete`     | All retries are finished.       |

<Tip>
  For jobs with thousands of addresses, poll every 10–30 seconds. Polling more frequently than necessary won't speed up processing and wastes API calls.
</Tip>

## Retrieving results

Once `status` is `complete`, fetch the full results:

```bash cURL theme={null}
curl --request GET \
  --url "https://api.orbisearch.com/v1/bulk/job_abc123/results" \
  --header "X-API-Key: YOUR_API_KEY"
```

Response:

```json theme={null}
{
  "job_id": "job_abc123",
  "status": "complete",
  "total_results": 500,
  "results": [
    {
      "email": "jane.doe@acme.com",
      "status": "safe",
      "substatus": "deliverable",
      "explanation": "The mailbox exists and is accepting mail.",
      "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
    }
  ],
  "pending_retries": [],
  "retry_count": 0
}
```

Each object in the `results` array follows the same schema as a single verification response. See [Handling Results](/docs/guides/handling-results) for guidance on acting on each status.

## Complete example

The following example submits a bulk job, polls until it completes, and prints the results.

<CodeGroup>
  ```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"}

  emails = [
      "jane.doe@acme.com",
      "info@example.com",
      "user@domain.org",
      # ... more addresses
  ]

  # Step 1: Submit the job
  submit_response = requests.post(
      f"{BASE_URL}/v1/bulk",
      headers=HEADERS,
      json=emails,
  )
  submit_response.raise_for_status()
  job = submit_response.json()
  job_id = job["job_id"]
  print(f"Job submitted: {job_id} ({job['total_emails']} emails, estimated cost: {job['estimated_cost']} credits)")

  # Step 2: Poll until complete
  while True:
      status_response = requests.get(
          f"{BASE_URL}/v1/bulk/{job_id}",
          headers=HEADERS,
      )
      status_response.raise_for_status()
      status = status_response.json()

      print(f"Status: {status['status']} — {status['emails_processed']}/{status['total_emails']} processed")

      if status["status"] == "complete":
          break
      elif status["status"] == "failed":
          raise RuntimeError(f"Job {job_id} failed.")

      time.sleep(15)  # poll every 15 seconds

  # Step 3: Retrieve results
  results_response = requests.get(
      f"{BASE_URL}/v1/bulk/{job_id}/results",
      headers=HEADERS,
  )
  results_response.raise_for_status()
  data = results_response.json()

  for result in data["results"]:
      print(f"{result['email']}: {result['status']} ({result['substatus']})")
  ```

  ```javascript JavaScript theme={null}
  const API_KEY = "YOUR_API_KEY";
  const BASE_URL = "https://api.orbisearch.com";
  const HEADERS = {
    "X-API-Key": API_KEY,
    "Content-Type": "application/json",
  };

  const emails = [
    "jane.doe@acme.com",
    "info@example.com",
    "user@domain.org",
    // ... more addresses
  ];

  async function sleep(ms) {
    return new Promise((resolve) => setTimeout(resolve, ms));
  }

  async function runBulkVerification() {
    // Step 1: Submit the job
    const submitRes = await fetch(`${BASE_URL}/v1/bulk`, {
      method: "POST",
      headers: HEADERS,
      body: JSON.stringify(emails),
    });
    const job = await submitRes.json();
    const { job_id } = job;
    console.log(`Job submitted: ${job_id} (${job.total_emails} emails, estimated cost: ${job.estimated_cost} credits)`);

    // Step 2: Poll until complete
    while (true) {
      const statusRes = await fetch(`${BASE_URL}/v1/bulk/${job_id}`, {
        headers: HEADERS,
      });
      const status = await statusRes.json();
      console.log(`Status: ${status.status} — ${status.emails_processed}/${status.total_emails} processed`);

      if (status.status === "complete") break;
      if (status.status === "failed") throw new Error(`Job ${job_id} failed.`);

      await sleep(15000); // poll every 15 seconds
    }

    // Step 3: Retrieve results
    const resultsRes = await fetch(`${BASE_URL}/v1/bulk/${job_id}/results`, {
      headers: HEADERS,
    });
    const data = await resultsRes.json();

    for (const result of data.results) {
      console.log(`${result.email}: ${result.status} (${result.substatus})`);
    }
  }

  runBulkVerification().catch(console.error);
  ```
</CodeGroup>

## Deduplication and credits

OrbiSearch deduplicates your submitted list before processing. If the same address appears more than once, it is verified once and the result is applied to all occurrences. You are charged **0.2 credits per unique email**.

The `estimated_cost` in the submit response reflects the deduplicated count.

<Warning>
  The `estimated_cost` is an estimate based on the unique email count at submission time. The final charge is determined at job completion.
</Warning>
