Batch & CSV search
Up to 1000 cached lookups per call, JSON or CSV, row in and row out, with per-row fault isolation.
Base URL https://api.offendersearch.appOne row in, one result out
Bulk cached lookups: each row runs through the same cached engine as POST /v1/criminal/search, so results are identical to running them individually, and every result carries the same Record schema and the same envelope — counts, warnings, pagination, labelled status. Live verification is not offered in batch — there is no live block on a batch row, and a batch never fans out to a live source, so every row is cache-only and every status is complete or error, never partial.
- Three accepted body shapes. A bare JSON array of query objects, a JSON envelope
{ "queries": [...], ...defaults }whose non-querieskeys apply to every row, or atext/csvbody whose header row names query fields. - Input order is preserved. Each result carries its 0-based
index, so you can zip results back onto your own rows without matching on identity. - 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; a batch of zero rows is a422 empty_batch.
meterEvents rolls up as a single criminal_call whose quantity is the number of rows that ran. A row that fails validation is not metered, so quantity can be lower than count.The JSON form
Post either a bare array of query objects, or an envelope { "queries": [...], ...defaults } whose non-queries keys are batch-wide defaults applied to every row. A per-row value overrides the default for that row — so nameStrategy: "prefix" at the envelope level applies to all rows unless a row sets its own. Each query object is flat — the fields from a query, not wrapped in { "query": … }.
curl -X POST https://api.offendersearch.app/v1/criminal/batch \
-H "X-API-Key: $OFFENDERSEARCH_KEY" \
-H "Content-Type: application/json" \
-d '[
{ "lastName": "Hamilton", "state": "TX" },
{ "firstName": "Alex", "state": "TX" }
]'{
"queries": [
{ "lastName": "Hamilton", "state": "TX" },
{ "lastName": "Example", "state": "IL", "firstName": "Chris" }
],
"include": ["sourceData"],
"nameStrategy": "prefix"
}The CSV form
Post a text/csv body whose header row names query fields, one query per line. Empty cells are dropped (they do not override a batch-wide default), and a UTF-8 BOM is handled. Unknown columns are ignored, so you can post a CSV export unchanged — a caseRef or note column is dropped rather than rejected. The endpoint still requires lastName per row.
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,caseRef\nAlex,Hamilton,TX,FILE-0142\nJordan,,IL,FILE-0143\n'{
"count": 2,
"results": [
{ "index": 0, "status": "complete",
"counts": { "records": 12, "recordsReturned": 12, "sourcesQueried": 0,
"sourcesComplete": 0, "sourcesIncomplete": 0 },
"warnings": [], "page": 1, "perPage": 50, "totalPages": 1,
"sourceStatus": [], "cappedOmittedSources": [], "omittedFields": ["sourceData"],
"records": [],
"legal": { "notice": "Not a consumer report. …" } },
{ "index": 1, "status": "error",
"error": "a lastName predicate is required — an unbounded query returns a lottery, not an answer",
"counts": { "records": 0, "recordsReturned": 0, "sourcesQueried": 0,
"sourcesComplete": 0, "sourcesIncomplete": 0 },
"records": [] }
],
"meterEvents": [ { "kind": "criminal_call", "quantity": 1, "chargeCeilingUsd": null } ]
}counts, exactly as a single search does — the unknown caseRef column was ignored. Row 1 has no surname, so it is status: "error" with the reason in place, and it is not billed: meterEvents.quantity is 1, for the one row that ran, even though count is 2.Guardrails & unbounded rows
A batch is one HTTP call, but each row is a full search and each row is validated on its own. The whole-batch guardrails are size limits; the per-row guardrail is the same surname requirement as a single search.
| Limit | Value | Over-limit behaviour |
|---|---|---|
| Max rows | 1000 | 413 batch_too_large |
| Zero rows | — | 422 empty_batch |
| Unparseable body / no CSV header | — | 422 bad_batch |
status: "error", not a lottery. A row with no lastName would be an unbounded query, so the row is returned with status: "error", its index in place, zeroed counts, an empty records, and an error string — and it is not billed. The rest of the batch still runs. A bad row is a row you can fix and resubmit, never a failure that costs you the whole call.