PrimaryCheck

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 }
EndpointWhat 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/batchList, newest first (?status=, ?reference=). Rows carry counts only.
POST /v1/verifications/batch/{id}/cancelCancels 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.csvOne 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));