Batch searches
One HTTP call, up to a thousand cached searches, one result per row in input order. Each row that runs is billed as a search, and no row can be live.
What you'll build
A roster screen: take a census, an applicant list or a daily visitor log, send it as one batch, and get back a result for every row — the same envelope a single POST /v1/criminal/search returns, minus the per-request keys. Rows that fail validation come back in place with the reason and do not stop the others. The batch engine is the cached engine, so a row's answer is identical to running it alone.
Before you start
- A test key (
os_test_…) onhttps://sandbox.offendersearch.app. Use the sandbox names:Felony Record(1975-06-15) returns one record,Clear Recordzero, and a row without a surname is anerrorrow. - Three input shapes: a JSON array of
queryobjects; a JSON envelope{ "queries": [...], ...defaults }where any other key (include,nameStrategy,state,perPage…) is a default applied to every row; ortext/csvwith a header row ofqueryfield names (empty cells dropped, a UTF-8 BOM tolerated, unknown columns ignored). - The reference: Batch & CSV.
searchId, meterEvents, usage and resolution. If you need a report or a history entry for a person, run that person through POST /v1/criminal/search instead. A live key in a batch is ignored; every row is cache-only, so a row is never partial.Step by step
- Build the rows. One
queryobject per person, each withlastNameand whatever else you hold (firstName,dob,state). Keep your own row ids outside the request and join onindex(0-based input position) when results return. - Send up to 1,000 rows.
POST /v1/criminal/batch. More than 1,000 is413 batch_too_large; zero rows is422 empty_batch; an unparseable body or a CSV without a header is422 bad_batch. - Read each row by status.
complete→ an envelope; readcounts.recordsandrecords[].error→errorsentence (anderrorCode: "query_too_broad"when that was the reason), zeroedcounts,records: [].not_run→ the row could not be searched within this request; it is listed inunfinished[]. - Resubmit unfinished rows. Collect
unfinished[].queryinto a new batch. Nothing was billed for them.
curl -s https://sandbox.offendersearch.app/v1/criminal/batch \
-H "X-API-Key: os_test_your_key" -H "Content-Type: application/json" \
-d '{
"queries": [
{ "lastName": "Record", "firstName": "Felony", "dob": "1975-06-15" },
{ "lastName": "Record", "firstName": "Clear" },
{ "firstName": "Nobody" }
],
"perPage": 50
}'{
"count": 3,
"results": [
{ "index": 0, "status": "complete",
"counts": { "records": 1, "recordsReturned": 1, "sourcesQueried": 0, "sourcesComplete": 0, "sourcesIncomplete": 0, "candidatesCapped": false, "exclusionsIncluded": false },
"warnings": [], "page": 1, "perPage": 50, "totalPages": 1,
"sourceStatus": [], "cappedOmittedSources": [], "omittedFields": ["sourceData"],
"records": [ { "recordCategory": "court", "matchState": "dob_match", "matchConfidence": 0.99, "…": "…" } ],
"proof": { "status": "none" },
"legal": { "notice": "Not a consumer report. …" } },
{ "index": 1, "status": "complete",
"counts": { "records": 0, "recordsReturned": 0, "sourcesQueried": 0, "sourcesComplete": 0, "sourcesIncomplete": 0, "candidatesCapped": false, "exclusionsIncluded": false },
"warnings": [], "page": 1, "perPage": 50, "totalPages": 1,
"sourceStatus": [], "cappedOmittedSources": [], "omittedFields": ["sourceData"],
"records": [], "proof": { "status": "none" }, "legal": { "notice": "Not a consumer report. …" } },
{ "index": 2, "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, "candidatesCapped": false, "exclusionsIncluded": false },
"records": [] }
],
"meterEvents": [
{ "kind": "criminal_call", "quantity": 1, "chargeCeilingUsd": null },
{ "kind": "criminal_call", "quantity": 1, "chargeCeilingUsd": null }
]
}Two rows ran and two criminal_call events were metered — one event per ran row, quantity: 1 each. The error row cost nothing. A production batch can also end with "unfinished": [ { "index": 7, "query": { … } } ] and a row { "index": 7, "status": "not_run", "error": "not run: this row could not be searched within this request…", "counts": { … }, "records": [] }.
Reading the result
| Field | Values | Read as |
|---|---|---|
count | integer | Always equals the number of input rows. |
results[].index | 0-based integer | The input position. Join on it; results are in input order but the index is the contract. |
results[].status | complete · error · not_run | Ran · failed validation (or was too broad) · could not be searched within this request. Never partial: there is no live in batch. |
results[].error | sentence, on error and not_run | The validation message, or the too-broad sentence, or the not-run notice. |
results[].errorCode | query_too_broad, present only on that case | Narrow the row with firstName or state. Other validation errors carry no code; the sentence names the field. |
unfinished[] | {index, query}, present only when rows were not run | What to resubmit. Nothing was billed for these rows. |
meterEvents[] | one criminal_call per ran row | Count them to reconcile; error and not_run rows emit none. |
row counts, records[], coverage | as on a search | A complete row is the search envelope minus searchId, meterEvents, usage, resolution; sourceStatus is always []. |
Edge cases and pitfalls
- `include` is read from the envelope only. A per-row
includeis ignored. Putinclude: ["coverage"]or["exclusions"]at the top level and it reaches every row. - `page`/`perPage` are validated per row. A row with
perPage: 300is anerrorrow namingperPage; the rest still run. - A too-broad row does not fail the batch. It comes back
status: "error"witherrorCode: "query_too_broad". Resubmit it alone with more fields. - Long batches are stitched. The service works through rows in parallel inside its budget; the gateway resubmits what did not finish for up to about five minutes and merges results by index. Only when that budget is spent do
not_runrows andunfinished[]reach you. Set a client timeout of at least 330 seconds. - A `live` default or a `live` on a row is ignored, not refused. If you need live verification for some people, run them individually after the batch.
- Clearances do not apply to batch rows.
subjectIdis honoured only on a synchronousPOST /v1/criminal/search. Apply your ownpersonRefsuppression to rows. - CSV columns are `query` field names.
firstName,lastName,dob,state,county,city,zipcode,birthYear,ageall work; a blank cell is dropped; a column the API does not know is ignored. SendContent-Type: text/csv— this is the one path where the gateway forwards your content type.
Production checklist
- Chunk at 1,000 rows and keep your own ids outside the request; join on
index. - Handle all three row statuses; resubmit
unfinished[]automatically. - Reconcile
meterEvents[](onecriminal_callper ran row) against rows withstatus: "complete". - Run people who need a
searchId(for a report or history), a live check, or clearances throughPOST /v1/criminal/search. - Treat
unverified: truerows as review items exactly as you would on a single search.