# Batch & CSV search

> Run up to 1000 cached lookups in one call. Post a JSON array, a JSON envelope with batch-wide defaults, or a text/csv body; results return in order, isolated.

- **HTML:** https://offendersearch.app/docs/criminal/batch
- **Base URL:** https://api.offendersearch.app
- **Authentication:** `X-API-Key` request header
- **OpenAPI:** https://offendersearch.app/openapi.json · https://offendersearch.app/openapi.yaml
- **Criminal reference as markdown:** https://offendersearch.app/docs/criminal.md

## One row in, one result out

Bulk **cached** lookups: each row runs through the same cached engine as `/v1/criminal/search`, so results are identical to running them individually, and every result carries the same envelope. **Live verification is not offered in batch** — every row is cache-only.

- **Three accepted body shapes.** A bare JSON array of query objects, a JSON envelope `{ "queries": [...], ...defaults }` whose non-`queries` keys apply to every row, or a `text/csv` body whose header row names query fields.
- **Input order is preserved.** Each result carries its 0-based `index`.
- **Rows are fault-isolated.** A row that fails validation is returned with `status: "error"` in place and never aborts the rest of the batch — and is not billed.
- **1000 rows per call.** A larger batch returns `413 batch_too_large`.

**Each row is billed as one call** — a 1,000-row batch is 1,000 calls. Only rows that ran are metered.

## The CSV form

Post a `text/csv` body whose header row names query fields. Unknown columns are ignored, so you can post a CSV export unchanged — a case id or note column is dropped rather than rejected. The endpoint still requires `lastName` per row.

```bash
curl -X POST "https://api.offendersearch.app/v1/criminal/batch" \
  -H "X-API-Key: $OFFENDERSEARCH_KEY" -H "Content-Type: text/csv" \
  --data-binary $'firstName,lastName,state\nAlex,Hamilton,TX\n'
```

```json
{
  "count": 2,
  "results": [
    { "index": 0, "status": "complete",
      "counts": { "records": 12, "recordsReturned": 12, "sourcesQueried": 0,
                  "sourcesComplete": 0, "sourcesIncomplete": 0 },
      "records": [] },
    { "index": 1, "status": "error",
      "error": "a lastName predicate is required — an unbounded query returns a lottery, not an answer",
      "counts": { "records": 0, "recordsReturned": 0 }, "records": [] }
  ],
  "meterEvents": [ { "kind": "criminal_call", "quantity": 1, "chargeCeilingUsd": null } ]
}
```

**Read each row's own `counts`.** Row-level completeness is reported per row, exactly as it is on a single search. The nameless row is `status: "error"` and is not billed — `meterEvents.quantity` is `1`, for the one row that ran.

---

## Related

- Previous: [Async live jobs & polling](https://offendersearch.app/docs/criminal/async.md)
- Next: [The Criminal Record object](https://offendersearch.app/docs/criminal/record-object.md)
- Index: [Criminal Search API reference](https://offendersearch.app/docs/criminal.md)
