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
| Python | Node.js | |
|---|---|---|
| Package | primarycheck | primarycheck |
| Runtime | 3.9+, sync and async clients | Node 18+, ESM and CommonJS, TypeScript types included |
| Dependencies | httpx only | none (fetch, node:crypto) |
| Sandbox | base_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.
| Class | HTTP | Typical codes |
|---|---|---|
AuthenticationError | 401 | missing_api_key, invalid_api_key, invalid_session |
PermissionDeniedError | 403 | fcra_not_enabled, purpose_not_certified, consent_required, read_only_role, session_required |
InvalidRequestError | 400 | validation_error, profession_unknown, cadence_invalid |
NotFoundError | 404 | verification_not_found, evidence_not_found |
ConflictError | 409 | idempotency_conflict, already_complete |
RateLimitError | 429 | rate_limited; carries retryAfter seconds |
APIError | 5xx | Our 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
| Resource | Python | Node.js |
|---|---|---|
| Verifications | pc.verifications.create / retrieve / list / cancel / report_pdf / evidence | pc.verifications.create / retrieve / list / cancel / reportPdf / evidence |
| Wait | pc.wait_for_completion(id, timeout=600, interval=None) | pc.waitForCompletion(id, { timeout, interval }) |
| Monitors | pc.monitors.create / retrieve / list / events / stop | pc.monitors.create / retrieve / list / events / stop |
| Webhook endpoints | pc.webhook_endpoints.create / list / delete / deliveries / retry | pc.webhookEndpoints.create / list / delete / deliveries / retry |
| Consents | pc.consents.create / retrieve | pc.consents.create / retrieve |
| Evidence | pc.evidence.retrieve / download | pc.evidence.retrieve / download |
| Catalog | pc.coverage(state, profession) · pc.professions() · pc.sources() | pc.coverage({ state, profession }) · pc.professions() · pc.sources() |
| Account | pc.account() · pc.usage(from_, to, group_by) | pc.account() · pc.usage({ from, to, groupBy }) |
| Webhooks | primarycheck.webhooks.verify_signature / construct_event | verifySignature / 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.