PrimaryCheck

Data retention and erasure

Two mechanisms remove personal data. Retention is the schedule: every report, evidence file and consent is disposed of by a nightly job once it ages past its policy. Erasure is on demand: when a customer passes on a deletion request (a departed candidate, a request under a state privacy law), one call redacts the subject's personal data today and keeps the non-personal audit skeleton the FCRA requires.

Retention schedule

A nightly job (03:00 America/New_York) reads the retention policies and deletes every row whose basis timestamp is older than the policy allows. Evidence blobs are deleted with their rows. An incomplete verification is never purged. Each run writes one audit row per table with the count, the cutoff and the policy, so disposal is provable in GET /v1/account/audit exports.

ObjectKept forClock starts at
Verifications (claims, source results, evidence cascade)7 yearscompletedAt
Evidence7 yearscapturedAt
Consents7 yearssignedAt
Webhook deliveries90 dayscreatedAt
Idempotency keys1 daycreatedAt

Erasing a verification

curl -X POST https://api.primarycheck.com/v1/verifications/ver_7c1e0b1f2a9d/erase \
  -H "Authorization: Bearer pc_live_…" 

The response is the redacted verification. What stays is exactly what an auditor needs to see that a report was ordered and what it said; what goes is everything that identifies the person.

Kept (audit skeleton)Erased
id, status, purpose, reference, metadata, createdAt, completedAtperson: names become "[erased]", date of birth, aliases, email and phone become null
Each claim's type, claimId, verdict, reasonClaim identifiers: licence numbers, NPIs, credential ids, schools, employers (only type, state, profession, issuer stay)
Each result's sourceCode, status, verdict, reason, checkedAtrecord, fieldChecks, summary
Each evidence entry's id, kind, sourceCode, sha256, capturedAtEvidence content, blobs in storage, sourceUrl; the report PDF
erasedAt, erasedBy (key:… or session:…), the audit-log rowResult-reuse cache entries for these results; the stored payloads of this verification's webhook events

Afterwards GET /v1/verifications/{id} returns the skeleton, report.pdf answers 410 verification_erased, and GET /v1/evidence/{id} answers 410 evidence_erased. A verification.erased webhook carries the skeleton. Consent records are not touched: the signed disclosure is the proof the order was lawful and is retained seven years.

When erasure is refused

CodeHTTPMeaning
verification_running409Still pending. Cancel it first, then erase.
adverse_action_open409A pre-adverse notice is waiting on this report. Send the final notice or withdraw, then erase.
dispute_open409A consumer dispute on this report is not resolved. The reinvestigation needs the report; erase once it closes.
already_erased409Erased earlier. Not idempotent by design so a double request is noticed.
verification_not_found404No such id on this account.

Monitors

POST /v1/monitors/{id}/erase stops the monitor (if it was active), replaces its person with [erased], strips identifiers from the claim, clears the snapshot and the diffs on its change events, and sets erasedAt. The monitor remains listed with its id, cadence, claim type and timestamps.

Everything about one person

curl -X POST https://api.primarycheck.com/v1/subjects/erase \
  -H "Authorization: Bearer pc_live_…" \
  -H "Content-Type: application/json" \
  -d '{
    "firstName": "Amy",
    "lastName": "Kowalski",
    "dob": "1976-02-14",
    "reference": "ATS-48213"
  }'
{ "object": "subject_erasure",
  "verifications": ["ver_7c1e0b1f2a9d", "ver_0a9d7c1e0b1f"],
  "monitors": ["mon_3f2a9d7c1e0b"],
  "skipped": [ { "id": "ver_b1f2a9d7c1e0", "object": "verification", "code": "dispute_open", "message": "…" } ],
  "erasedCount": 3 }

Matching is by first and last name (case, accents and punctuation ignored), narrowed by dob (recommended) and by your reference when given, on your account only. Each match is erased exactly as the single-object routes would; matches that cannot be erased now are listed under skipped with the reason code and left untouched, so call again once the process on them is closed. No match is a 200 with empty lists.

Dashboard and SDKs

In the dashboard, a verification's detail page and each monitor carry Erase personal data behind a confirmation step. In the SDKs: pc.verifications.erase(id), pc.monitors.erase(id), pc.subjects.erase(first_name, last_name, dob=…, reference=…) (Python, sync and async) and pc.verifications.erase(id), pc.monitors.erase(id), pc.subjects.erase({ firstName, lastName, dob, reference }) (Node). Every erasure is written to the audit log with the actor and is visible in the audit export.