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

# Bulk Intelligence

> Upload a file of leads or companies and enrich every row in a single job.

## Use case

Enrich thousands of records at once without managing per-row API calls. Upload a CSV or JSON file, define what you want back with a `struct`, and download the enriched results when the job completes. Use it for list building, CRM backfills, and large-scale data refreshes.

## Endpoint

Use the people endpoint for lead/person rows:

```http theme={null}
POST https://api.sixtyfour.ai/bulk-intelligence/people
```

Use the company endpoint for company rows:

```http theme={null}
POST https://api.sixtyfour.ai/bulk-intelligence/company
```

<CardGroup cols={2}>
  <Card title="People API Reference" icon="code" href="/api-reference/enrichment/people-intelligence-bulk">
    Full request/response schema for `POST /bulk-intelligence/people`.
  </Card>

  <Card title="Company API Reference" icon="code" href="/api-reference/enrichment/company-intelligence-bulk">
    Full request/response schema for `POST /bulk-intelligence/company`.
  </Card>
</CardGroup>

## Pricing

See [Credits & Pricing Guide](/guides/credits-and-pricing) for credit costs by tier.

<Note>
  Before the job starts, Sixtyfour checks that your balance covers the estimated cost. If it doesn't, the request returns 402 and nothing is charged. The submit response includes `estimated_cost_cents`. The completed `/job-status` response includes `charge_amount` in cents.
</Note>

## Errors

For error responses (400, 402, 403, 422, etc.), see [Handling Errors](/api-reference/errors).

## Tiers

Allowed tiers depend on the endpoint:

| Endpoint                     | Tiers                                               |
| ---------------------------- | --------------------------------------------------- |
| `/bulk-intelligence/people`  | `micro`, `low` (default), `medium`, `high`, `xhigh` |
| `/bulk-intelligence/company` | `micro`, `low` (default), `medium`, `high`          |

## Request format

Both endpoints take `multipart/form-data` with two parts:

| Part     | Description                                                                    |
| -------- | ------------------------------------------------------------------------------ |
| `file`   | The records to enrich. Accepts `.csv`, `.json`, `.jsonl`, or `.ndjson`.        |
| `config` | A JSON-encoded string describing the enrichment — what to return for each row. |

### Limits

| Limit         | Value                 |
| ------------- | --------------------- |
| File size     | 100 MB                |
| Rows per file | 100,000               |
| Config size   | 300,000 characters    |
| Submissions   | 10 per minute per org |

## The `config` object

```json theme={null}
{
  "struct": {
    "email": "The individual's email address",
    "linkedin": "LinkedIn URL for the person"
  },
  "tier": "low",
  "field_confidence": true,
  "result_formats": ["csv", "ndjson"],
  "webhook_url": "https://your-server.com/webhooks/sixtyfour"
}
```

For struct value formats, supported types, and casting, see [Struct & Type Casting](/guides/struct-and-type-casting).

`result_formats` controls output files. CSV is always produced regardless of this field; add `"ndjson"` to also save typed values — numbers, booleans, and nested objects survive instead of being flattened to strings. When you request `ndjson`, the job-status `results` array contains one entry per format at the same `block_number` — match the `filename` extension (`.csv` or `.ndjson`) to pick the file you need.

<Warning>
  `high` and `xhigh` are gated — access is granted case-by-case by our team. `xhigh` is available on people bulk only. Requests for a tier your org doesn't have access to return 403 — see [Handling Errors](/api-reference/errors#403-forbidden).
</Warning>

## Submitting a job

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST "https://api.sixtyfour.ai/bulk-intelligence/people" \
    -H "x-api-key: YOUR_API_KEY" \
    -F "file=@leads.csv" \
    -F 'config={
      "struct": {
        "email": "The individual'\''s email address",
        "linkedin": "LinkedIn URL for the person",
        "title": "The individual'\''s job title"
      }
    }'
  ```

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

  config = {
      "struct": {
          "email": "The individual's email address",
          "linkedin": "LinkedIn URL for the person",
          "title": "The individual's job title"
      }
  }

  with open("leads.csv", "rb") as f:
      response = requests.post(
          "https://api.sixtyfour.ai/bulk-intelligence/people",
          headers={"x-api-key": "YOUR_API_KEY"},
          files={"file": ("leads.csv", f, "text/csv")},
          data={"config": json.dumps(config)}
      )

  response.raise_for_status()
  task_id = response.json()["task_id"]
  ```

  ```javascript JavaScript theme={null}
  import { readFile } from "node:fs/promises";

  const form = new FormData();
  form.append("file", new Blob([await readFile("leads.csv")], { type: "text/csv" }), "leads.csv");
  form.append("config", JSON.stringify({
    struct: {
      email: "The individual's email address",
      linkedin: "LinkedIn URL for the person",
      title: "The individual's job title"
    }
  }));

  const response = await fetch("https://api.sixtyfour.ai/bulk-intelligence/people", {
    method: "POST",
    headers: { "x-api-key": "YOUR_API_KEY" },
    body: form
  });

  if (!response.ok) throw new Error(`Submission failed: ${response.status}`);
  const { task_id } = await response.json();
  ```
</CodeGroup>

The response includes the job handle and the cost estimate:

```json theme={null}
{
  "task_id": "9b1f9d2e-...",
  "status": "RUNNING",
  "row_count": 2500,
  "estimated_cost_cents": 25000
}
```

## Polling for results

Poll `GET /job-status/{task_id}` until `status` is `completed`, `failed`, or `cancelled`. When the job completes, the response includes a `results` array of signed download links for the output files plus `charge_amount` in cents — bulk jobs deliver files, not an inline `result`.

The `results` array contains one entry per pipeline stage; the final enriched output is the entry with the highest `block_number`. Small files also carry an inline `results` row preview alongside the link — use one or the other, not both.

<Note>Download links are signed URLs that expire after 15 minutes — poll again for fresh links — and require your `x-api-key` header when fetching.</Note>

<CodeGroup>
  ```bash cURL theme={null}
  # Check status
  curl "https://api.sixtyfour.ai/job-status/TASK_ID" \
    -H "x-api-key: YOUR_API_KEY"

  # When status is "completed", download the result (no redirects — pass your key directly)
  curl -o enriched.csv \
    -H "x-api-key: YOUR_API_KEY" \
    "SIGNED_DOWNLOAD_URL"
  ```

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

  deadline = time.monotonic() + 2 * 60 * 60  # give up after 2 hours

  while True:
      poll_resp = requests.get(
          f"https://api.sixtyfour.ai/job-status/{task_id}",
          headers={"x-api-key": "YOUR_API_KEY"}
      )
      poll_resp.raise_for_status()
      status = poll_resp.json()

      if status["status"] == "completed":
          # The final stage has the highest block_number. If you requested NDJSON,
          # CSV and NDJSON share that block_number — pick by filename extension.
          final_block = max(f["block_number"] or 0 for f in status["results"])
          suffix = ".csv"  # use ".ndjson" if you requested NDJSON output
          final = next(
              (f for f in status["results"]
               if (f["block_number"] or 0) == final_block and f["filename"].endswith(suffix)),
              None,
          )
          if final is None:
              raise RuntimeError(f"No {suffix} file in results")
          download = requests.get(
              final["download_url"],
              headers={"x-api-key": "YOUR_API_KEY"},
              # Downloads are served directly by api.sixtyfour.ai; never
              # forward your API key across a redirect.
              allow_redirects=False
          )
          if download.status_code != 200:
              raise RuntimeError(f"Download failed: {download.status_code}")
          with open(final["filename"], "wb") as out:
              out.write(download.content)
          break
      if status["status"] in ("failed", "cancelled"):
          raise RuntimeError(f"Job {status['status']}: {status.get('error', 'Unknown error')}")
      if time.monotonic() > deadline:
          raise TimeoutError("Job didn't complete within 2 hours")

      time.sleep(30)
  ```

  ```javascript JavaScript theme={null}
  const deadline = Date.now() + 2 * 60 * 60 * 1000; // give up after 2 hours

  while (true) {
    const pollResponse = await fetch(
      `https://api.sixtyfour.ai/job-status/${task_id}`,
      { headers: { "x-api-key": "YOUR_API_KEY" } }
    );
    if (!pollResponse.ok) throw new Error(`Status check failed: ${pollResponse.status}`);
    const status = await pollResponse.json();

    if (status.status === "completed") {
      // The final stage has the highest block_number. If you requested NDJSON,
      // CSV and NDJSON share that block_number — pick by filename extension.
      const finalBlock = Math.max(...status.results.map(f => f.block_number ?? 0));
      const suffix = ".csv"; // use ".ndjson" if you requested NDJSON output
      const final = status.results.find(
        f => (f.block_number ?? 0) === finalBlock && f.filename.endsWith(suffix)
      );
      if (!final) throw new Error(`No ${suffix} file in results`);
      // Downloads are served directly by api.sixtyfour.ai; redirect: "error"
      // ensures your API key is never forwarded across a redirect.
      const data = await fetch(final.download_url, {
        headers: { "x-api-key": "YOUR_API_KEY" },
        redirect: "error"
      });
      if (!data.ok) throw new Error(`Download failed: ${data.status}`);
      // write data to disk or stream it onward
      break;
    }
    if (["failed", "cancelled"].includes(status.status)) {
      throw new Error(`Job ${status.status}: ${status.error || "Unknown error"}`);
    }
    if (Date.now() > deadline) {
      throw new Error("Job didn't complete within 2 hours");
    }
    await new Promise(r => setTimeout(r, 30000));
  }
  ```
</CodeGroup>

<Tip>
  For per-block progress while the job runs, use [Workflow Run Live Status](/api-reference/workflow/get-workflow-run-live-status).
</Tip>

## Webhook callback

Pass a `webhook_url` in the config to receive results via HTTP POST when the job completes, instead of polling. The payload includes signed download URLs for the result files. The signed payload, retry behavior, and verification steps are documented in [Outgoing Webhooks](/api-reference/webhooks/outgoing).

<Note>Webhook URLs must use HTTPS and resolve to a public address. The URL is validated when you submit and again at delivery time.</Note>

## Idempotency

Send an `Idempotency-Key` header (up to 255 characters) to make submissions retry-safe. Resubmitting with the same key returns the original job instead of starting a new one:

```json theme={null}
{
  "task_id": "9b1f9d2e-...",
  "status": "RUNNING",
  "already_started": true
}
```

Idempotent replays don't count against the submission rate limit, and `row_count` and `estimated_cost_cents` are omitted, since the original upload is authoritative.

## Failure behavior

Rows that fail enrichment pass through to the output un-enriched — a single hard row doesn't fail the job. If the uploaded file itself can't be read or parsed, the request fails with 400 before anything runs.
