Batch searches
One HTTP call, many searches, results in input order. Each row is a search like any other — billed like one, and read like one.
When to use this
A roster upload, an overnight re-screen of a client list, a spreadsheet from a facility: when you hold many people at once and want one request, POST /v1/batch runs up to 1,000 queries through the same engine as POST /v1/search with bounded server-side concurrency and returns one result per row, in order. You will build a loader that accepts a CSV or a list of people, submits it, and writes one outcome per row — including the rows that were refused.
Before you start
- A test key on the sandbox. Rows use the same test names; this guide batches Exact Match, Clear Visitor, Name Only and Out State.
- Three body shapes: a bare JSON array of
Queryobjects; a JSON envelope{"queries": [...], ...options}where the options (jurisdictions,match,recordTypes,locationScoped,include,deadlineMs,onDeadline,subjectId,clientId,clearanceMode) apply to every row; or atext/csvbody whose header row namesQueryfields. - Batch is Regular only: no
live, noproof. Each row is billed as one search (pricing). Limits: more than 1,000 rows is a413; an empty batch is a422; a trial account whose remaining searches are fewer than the rows gets a402.
POST /v1/report, re-opened at /results, or named in a clearance. For a row you will need to cite — a hit you must document — run POST /v1/search for that person after the batch.Step by step
- Submit a JSON envelope. Four rows and one batch-wide option. The response has
countandresults[], one entry per row,index0-based. - Submit the same people as CSV. Header row of
Queryfield names, one person per line,Content-Type: text/csv. Unknown CSV columns are ignored. - Read each row.
statusiscomplete,partialorerror. Acomplete/partialrow hascounts,warningsandrecords; anerrorrow haserrorandrecords: []. - Follow up on hits. For each row with records you must act on, run
POST /v1/searchto get a citablesearchId, then report or clear from it.
Step 1 — JSON envelope
curl -s https://sandbox.offendersearch.app/v1/batch \
-H "X-API-Key: $OFFENDERSEARCH_KEY" -H "Content-Type: application/json" \
-d '{"queries": [
{"firstName": "Exact", "lastName": "Match", "dob": "1980-01-01"},
{"firstName": "Clear", "lastName": "Visitor"},
{"firstName": "Name", "lastName": "Only"},
{"firstName": "Out", "lastName": "State", "state": "TX"}
],
"match": "balanced", "onDeadline": "partial"}'{
"count": 4,
"results": [
{ "index": 0, "status": "complete",
"counts": { "records": 1, "recordsReturned": 1, "sourcesQueried": 58, "sourcesComplete": 58, "sourcesIncomplete": 0, "sourcesSkippedByScope": 0 },
"warnings": [],
"records": [ { "recordId": "rec_28db9639d712c4c61e8e", "personRef": "prs_28db9639d712c4c61e8e", "matchState": "dob_match", "matchConfidence": 0.9, "registrationState": "OH" } ] },
{ "index": 1, "status": "complete",
"counts": { "records": 0, "recordsReturned": 0, "sourcesQueried": 58, "sourcesComplete": 58, "sourcesIncomplete": 0, "sourcesSkippedByScope": 0 },
"warnings": [], "records": [] },
{ "index": 2, "status": "complete",
"counts": { "records": 3, "recordsReturned": 3, "sourcesQueried": 58, "sourcesComplete": 58, "sourcesIncomplete": 0, "sourcesSkippedByScope": 0 },
"warnings": [],
"records": [ { "personRef": "prs_fec5ef71d9deef5bc8c4", "matchState": null, "matchConfidence": 0.6, "registrationState": "AZ" },
{ "personRef": "prs_ca91307a7124dca36777", "matchState": null, "matchConfidence": 0.6, "registrationState": "GA" },
{ "personRef": "prs_84881c51922112e393d2", "matchState": null, "matchConfidence": 0.6, "registrationState": "MI" } ] },
{ "index": 3, "status": "complete",
"counts": { "records": 1, "recordsReturned": 1, "sourcesQueried": 58, "sourcesComplete": 58, "sourcesIncomplete": 0, "sourcesSkippedByScope": 0 },
"warnings": [],
"records": [ { "recordId": "rec_21b7a06132139de2ba35", "personRef": "prs_21b7a06132139de2ba35", "matchState": null, "matchConfidence": 0.9, "registrationState": "OK" } ] }
],
"sandbox": true
}Step 2 — CSV
cat > roster.csv <<'CSV'
firstName,lastName,dob,state
Exact,Match,1980-01-01,
Clear,Visitor,,
Out,State,,TX
CSV
curl -s https://sandbox.offendersearch.app/v1/batch \
-H "X-API-Key: $OFFENDERSEARCH_KEY" -H "Content-Type: text/csv" \
--data-binary @roster.csvThe header names Query fields (firstName,lastName,dob,age,city,county,state,zipcode,address,q,…); the optional columns subjectId, clientId and clearanceMode turn clearances on for that row. Empty cells are empty values. The response is the same {count, results[]} shape.
Step 3 — rows that are refused
// [{"firstName": "Exact", "lastName": "Match", "dob": "1980"}]
{ "detail": "Row 0: dob: Datetimes provided to dates should have zero time - e.g. be exact dates" }The distinction: a JSON row that cannot be read (wrong type, unknown key) makes the whole request a 422 whose detail names the row index, and nothing runs. A row that reads but is refused when it runs is reported in place with status: "error", and the rest of the batch is unaffected. error is usually a sentence; for a coded refusal (such as the location-record quota) it is a {code, message} object.
Reading the result
| Field | Values | What it decides per row |
|---|---|---|
results[].index | 0-based integer | Join back to your input; results are in input order, but key on this anyway. |
results[].status | complete, partial, error | Same meaning as the envelope status; error means the row did not run. |
results[].counts | the full SearchResponse counts | sourcesIncomplete > 0 makes the row a lower bound. Check it per row — one row can be short while the rest are whole. |
results[].warnings[] | sentences | The row's only prose completeness signal: a row carries no sourceStatus. |
results[].records[] | Record | Read exactly as a synchronous result; personRef, matchState, matchDetail all present. |
results[].cleared[], counts.clearedSuppressed | only with subjectId on the row or envelope | What a not_this_person clearance removed from this row. |
results[].error | string or {code, message} | Why the row did not run. |
| Not present | searchId, sourceStatus, page*, capped*, proof | Rows are not citable and not paged; see the callout above. |
Edge cases and pitfalls
- One hit per row does not mean one person. A
Name Onlyrow returns three candidates; your loader must store a list per row, not a flag. - `live` in a row is a `422` (unknown key); at the envelope level it is ignored. Batch is Regular. Re-check hits live with
POST /v1/search. - CSV and JSON treat unknown fields differently: CSV columns the API does not know are ignored; a JSON key it does not know refuses the batch. Validate your mapping on a one-row batch first.
- A `dob` cell of `1980` (year only) refuses the whole batch. Clean dates to
YYYY-MM-DDor move the year toage. - Envelope options bind every row.
jurisdictionson the envelope scopes every search in it;subjectIdon the envelope applies the same subject's clearances to every row — almost never what you want. PutsubjectIdon the row. - Timeouts. A 1,000-row batch is 1,000 searches at bounded concurrency; set your HTTP timeout accordingly (minutes), or split into smaller batches you can retry independently.
- Billing is per row, not per request: 100 rows is 100 searches at the graduated per-call rate; a trial cannot run a batch larger than its remaining searches.
Production checklist
- Rows validated locally against the
Queryfield list before submit; dates normalised. - Per-row
status,counts.sourcesIncomplete,warningsandrecordsstored byindex. - Hits that need citing are re-run with
POST /v1/searchand thesearchIdkept. - Batches sized so one failure is cheap to retry; HTTP timeout sized to the batch.
Related products
Product overview
- Sex offender search API
- Sex offender registry API
- Offender search API
- Background check API
- Bulk sex offender search
- Background check API pricing
- Background check API integration
- Integrations
- Free background check API
- Pay-as-you-go background check API
- Background check API in Python
- Background check API in Node.js
- NSOPW API