Batch verifications
Staffing agencies and HR teams rarely verify one person. POST /v1/verifications/batch takes up to 500 create bodies and returns one batch object to track. Every item is an ordinary verification — same validation, same FCRA gate, same evidence, same report PDF, same per-item webhooks — created through exactly the path the single endpoint uses, with batchId set. In the dashboard the same thing is a CSV upload under Batches → Bulk upload.
curl -X POST https://api.primarycheck.com/v1/verifications/batch \
-H "Authorization: Bearer pc_live_…" \
-H "Content-Type: application/json" \
-d '{
"items": [
{
"person": {
"firstName": "Amy",
"lastName": "Kowalski",
"dob": "1976-02-14"
},
"claims": [
{
"type": "license",
"state": "PA",
"profession": "rn",
"number": "RN353077L"
},
{
"type": "exclusion_screen",
"npi": "1003000126"
}
],
"purpose": "employment",
"consent": {
"consentId": "cons_8f1…"
},
"reference": "EMP-1001"
},
{
"person": {
"firstName": "Luis",
"lastName": "Ortega",
"dob": "1988-09-03"
},
"claims": [
{
"type": "healthcare_screen",
"state": "OH",
"profession": "md",
"licenseNumber": "35123456"
}
],
"purpose": "employment",
"consent": {
"consentId": "cons_8f2…"
},
"reference": "EMP-1002"
}
],
"reference": "roster-2026-11",
"options": {
"maxAge": "P1D"
}
}'Validated as a whole
If any item is invalid, nothing is created and the error lists every bad item, so a 400-row file is fixed in one pass instead of forty. error.errors[] carries index, code, message and a param such as items[3].person.lastName. The envelope is the usual error shape with code batch_invalid (400) — or batch_rejected (403, type fcra) when an item fails the consent or purpose gate. An account that has not completed vetting gets fcra_not_enabled, as on the single endpoint.
{ "error": { "type": "invalid_request", "code": "batch_invalid", "param": "items",
"message": "2 of 40 items were rejected; no verification was created. See error.errors[] for each one.",
"errors": [ { "index": 3, "code": "validation_error", "message": "Field required", "param": "items[3].person.lastName" },
{ "index": 17, "code": "profession_unknown", "message": "Unknown profession 'wizard'…", "param": "items[17].claims[0].profession" } ] } }The batch object
{ "id": "bat_…", "object": "batch", "status": "pending", "mode": "live", "reference": "roster-2026-11", "options": { "maxAge": "P1D" },
"itemCount": 40,
"counts": { "queued": 12, "running": 3, "pending": 15, "complete": 24, "cancelled": 1, "total": 40 },
"verdicts": { "verified": 20, "discrepancy": 2, "not_found": 1, "unable_to_verify": 2, "pending": 15 },
"items": { "data": [ { "id": "ver_…", "index": 0, "status": "complete", "reference": "EMP-1001",
"person": { "firstName": "Amy", "lastName": "Kowalski", "dob": "1976-02-14" },
"verdict": "verified", "reason": null,
"claims": [ { "claimIndex": 0, "type": "license", "verdict": "verified", "reason": null, "summary": "license PA rn RN353077L" } ],
"createdAt": "…", "completedAt": "…" } ], "hasMore": false },
"createdAt": "…", "completedAt": null }| Endpoint | What it does |
|---|---|
GET /v1/verifications/batch/{id} | Counts, verdict tallies and a page of items in submission order. ?wait=25 long-polls until the batch completes; ?limit= (≤500), ?startingAfter= and ?status= page and filter the items. |
GET /v1/verifications/batch | List, newest first (?status=, ?reference=). Rows carry counts only. |
POST /v1/verifications/batch/{id}/cancel | Cancels every item still pending — each through the single cancel — and leaves finished items alone. 409 already_complete when nothing is pending. |
GET /v1/verifications/batch/{id}/export.csv | One row per item: id, index, reference, name, dob, claim summary with verdicts, verdict, reason, status, timestamps, report link. |
An item's verdict rolls its claims up the way the report does: discrepancy if any claim is, else pending if any is, else verified only when every claim verified, else not_found, else unable_to_verify. options on the batch are defaults for every item (an item's own options win per key). Idempotency-Key behaves exactly as on the single endpoint. Rate limits count the batch as one request; items are fanned out to the queue from the worker, so a 500-item batch returns in the time it takes to store 500 rows.
Completion
A batch is complete when no item is pending; cancelled items count as finished. batch.completed fires once, when the last item finishes, with the batch object (counts, tallies, items) as data.object; every item still fires its own verification.completed. Subscribe with events: ["batch.completed"] on a webhook endpoint. In the sandbox the magic last names apply per item: a Pending item keeps the batch pending until you cancel.
SDKs
# Python
b = pc.verifications.create_batch(items=[...], reference="roster-2026-11", options={"max_age": "P1D"})
b = pc.batches.wait_for_completion(b["id"]) # long-polls GET …/batch/{id}?wait=25
open("roster.csv", "wb").write(pc.batches.export_csv(b["id"]))
// Node
const b = await pc.verifications.createBatch({ items, reference: "roster-2026-11", options: { maxAge: "P1D" } });
const done = await pc.batches.waitForCompletion(b.id);
await fs.promises.writeFile("roster.csv", await pc.batches.exportCsv(b.id));