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.
| Object | Kept for | Clock starts at |
|---|---|---|
| Verifications (claims, source results, evidence cascade) | 7 years | completedAt |
| Evidence | 7 years | capturedAt |
| Consents | 7 years | signedAt |
| Webhook deliveries | 90 days | createdAt |
| Idempotency keys | 1 day | createdAt |
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, completedAt | person: names become "[erased]", date of birth, aliases, email and phone become null |
Each claim's type, claimId, verdict, reason | Claim identifiers: licence numbers, NPIs, credential ids, schools, employers (only type, state, profession, issuer stay) |
Each result's sourceCode, status, verdict, reason, checkedAt | record, fieldChecks, summary |
Each evidence entry's id, kind, sourceCode, sha256, capturedAt | Evidence content, blobs in storage, sourceUrl; the report PDF |
erasedAt, erasedBy (key:… or session:…), the audit-log row | Result-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
| Code | HTTP | Meaning |
|---|---|---|
verification_running | 409 | Still pending. Cancel it first, then erase. |
adverse_action_open | 409 | A pre-adverse notice is waiting on this report. Send the final notice or withdraw, then erase. |
dispute_open | 409 | A consumer dispute on this report is not resolved. The reinvestigation needs the report; erase once it closes. |
already_erased | 409 | Erased earlier. Not idempotent by design so a double request is noticed. |
verification_not_found | 404 | No 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.