PrimaryCheck

SDKs

Two official clients, both version 0.1.0, both thin: they send exactly the JSON documented in the API reference and return it unchanged (typed in TypeScript, plain dicts in Python). Each adds the four things every integration otherwise rewrites: an Idempotency-Key on every POST, retries with backoff that honour Retry-After, waitForCompletion on top of ?wait=25, and timing-safe webhook signature verification.

Install

pip install primarycheck
PythonNode.js
Packageprimarycheckprimarycheck
Runtime3.9+, sync and async clientsNode 18+, ESM and CommonJS, TypeScript types included
Dependencieshttpx onlynone (fetch, node:crypto)
Sandboxbase_url="https://sandbox.primarycheck.com"baseUrl: "https://sandbox.primarycheck.com"

Create a verification and wait for it

Pass wait (up to 25 seconds) so board and dataset checks come back complete in one call. When a claim needs employer or registrar outreach the verification stays pending; waitForCompletion long-polls GET /v1/verifications/{id}?wait=25 until it is final or your timeout passes (default ten minutes), and webhooks remain the right answer for anything longer.

from primarycheck import PrimaryCheck

pc = PrimaryCheck("pc_live_…")   # base_url="https://sandbox.primarycheck.com" for pc_test_ keys

v = pc.verifications.create(
    person={"firstName": "Amy", "middleName": "E", "lastName": "Kowalski", "dob": "1976-02-14"},
    claims=[
        {"type": "license", "state": "PA", "profession": "rn", "number": "RN353077L", "claimId": "c1"},
        {"type": "npi", "number": "1003000126"},
        {"type": "exclusion_screen", "npi": "1003000126"},
    ],
    purpose="employment",
    consent={"consentId": "cons_8f1…"},
    reference="ATS-48213",
    wait=25,                       # long-poll up to 25 s: board + dataset checks usually finish here
)
if v["status"] == "pending":       # outreach can take days; webhooks are the better answer
    v = pc.wait_for_completion(v["id"], timeout=600)

for c in v["claims"]:
    print(c["type"], c["verdict"], c.get("reason"))

List with auto-pagination

List calls return one page (data, hasMore). Iterating the page in Node (for await) or calling auto_paging_iter() in Python follows startingAfter cursors until the end.

page = pc.verifications.list(status="complete", created_after="2026-11-01", limit=50)
for v in page:                      # this page only
    ...
for v in page.auto_paging_iter():   # every page, following startingAfter cursors
    print(v["id"], v["reference"])

# async
async with AsyncPrimaryCheck("pc_live_…") as pc:
    async for v in await pc.verifications.list(status="pending"):
        ...

Verify a webhook

Verify the raw request body, before any JSON parsing. The function returns the parsed event on success and throws SignatureVerificationError otherwise. Tolerance defaults to 300 seconds. Both implementations agree byte for byte with the server's signer; see Webhooks.

from primarycheck import webhooks, SignatureVerificationError

@app.post("/primarycheck")                                  # FastAPI; Flask/Django are the same idea
async def hook(request: Request):
    try:
        event = webhooks.verify_signature(
            await request.body(),                           # the RAW bytes, before JSON parsing
            request.headers["PrimaryCheck-Signature"],
            WEBHOOK_SECRET,                                 # whsec_… from POST /v1/webhook_endpoints
        )
    except SignatureVerificationError:
        return Response(status_code=400)
    if event["type"] == "verification.completed":
        verification = event["data"]["object"]
    return Response(status_code=200)

Errors

One class per HTTP status, all subclasses of PrimaryCheckError, each carrying code, message, type, status, requestId and param straight from the error body.

ClassHTTPTypical codes
AuthenticationError401missing_api_key, invalid_api_key, invalid_session
PermissionDeniedError403fcra_not_enabled, purpose_not_certified, consent_required, read_only_role, session_required
InvalidRequestError400validation_error, profession_unknown, cadence_invalid
NotFoundError404verification_not_found, evidence_not_found
ConflictError409idempotency_conflict, already_complete
RateLimitError429rate_limited; carries retryAfter seconds
APIError5xxOur fault. Quote requestId to support.
APIConnectionError—No HTTP response after every retry (DNS, TLS, timeout).
WaitTimeoutError—waitForCompletion gave up; the verification is still pending.
SignatureVerificationError—A webhook signature did not verify.
from primarycheck import PrimaryCheckError, InvalidRequestError, RateLimitError

try:
    v = pc.verifications.create(person=..., claims=..., purpose="employment", consent=...)
except InvalidRequestError as e:
    print(e.code, e.param, e.message)      # profession_unknown claims[0].profession Unknown profession 'wizard'…
except RateLimitError as e:
    time.sleep(e.retry_after or 1)         # the SDK already retried twice honouring Retry-After
except PrimaryCheckError as e:
    log.error("%s %s request_id=%s", e.status, e.code, e.request_id)

Retries

maxRetries (default 2) applies to 429, 5xx and network errors. A 429 waits exactly Retry-After; everything else backs off 0.5 s, 1 s, 2 s … with jitter, capped at 30 s. 4xx responses are never retried. The per-request timeout is 30 s and long-polls add their wait on top automatically.

Idempotency

Every POST carries an Idempotency-Key (UUID v4) generated once per call and re-sent on every retry of that call, so a retried create never orders twice. Pass your own key (idempotency_key= / { idempotencyKey }) when the key should survive a process restart, for example your ATS request id. See Idempotency.

Version pinning

version="2026-11-03" / { version: "2026-11-03" } sends PrimaryCheck-Version on every request; otherwise the account's pinned version applies. See Versioning.

Surface

ResourcePythonNode.js
Verificationspc.verifications.create / retrieve / list / cancel / report_pdf / evidencepc.verifications.create / retrieve / list / cancel / reportPdf / evidence
Waitpc.wait_for_completion(id, timeout=600, interval=None)pc.waitForCompletion(id, { timeout, interval })
Monitorspc.monitors.create / retrieve / list / events / stoppc.monitors.create / retrieve / list / events / stop
Webhook endpointspc.webhook_endpoints.create / list / delete / deliveries / retrypc.webhookEndpoints.create / list / delete / deliveries / retry
Consentspc.consents.create / retrievepc.consents.create / retrieve
Evidencepc.evidence.retrieve / downloadpc.evidence.retrieve / download
Catalogpc.coverage(state, profession) · pc.professions() · pc.sources()pc.coverage({ state, profession }) · pc.professions() · pc.sources()
Accountpc.account() · pc.usage(from_, to, group_by)pc.account() · pc.usage({ from, to, groupBy })
Webhooksprimarycheck.webhooks.verify_signature / construct_eventverifySignature / constructEvent

Both packages ship from the sdks/ directory of the API repository and are tested against a mocked transport (httpx.MockTransport, a mocked fetch), so the request shapes above are what they send.