Data retention
Configure how long TokenID keeps event payloads and audit-log entries, with an immutable deletion registry for every purge.
Every organization has independent retention windows for raw event data and for the audit log, plus two erasure endpoints for GDPR right-to-be-forgotten requests. Every purge — scheduled or on-demand — writes an immutable row to the deletion registry, so you always have proof-of-purge for GDPR, HIPAA, or SOC 2 evidence requests.
Configure retention
/api/v1/org/{org_id}/retention{
"org_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"events_retention_days": 365,
"audit_log_retention_days": 2555,
"updated_at": "2026-05-21T12:00:00+00:00"
}
Fields
| Field | Range | Default | Notes |
|---|---|---|---|
events_retention_days |
1–3650 | 365 | Raw event ledger window |
audit_log_retention_days |
1–3650 | 2555 | ~7 years — common SOX floor |
Update example:
curl -X PUT https://token.audit.id/api/v1/org/{org_id}/retention \
-H "Authorization: Bearer td_live_xxxx" \
-H "Content-Type: application/json" \
-d '{"events_retention_days": 90}'
A nightly trim job deletes rows older than each cutoff, scoped per-org. The dashboard surfaces
a Last purge indicator on Settings → Data Retention so you can confirm the job is healthy.
Deletion registry
Every purge — nightly trim, subject erasure, or full org erasure — writes a row to the
immutable deletion_registry table capturing:
| Column | Description |
|---|---|
org_id |
The org whose data was purged |
actor_id |
The user who triggered the deletion (null for the nightly job) |
reason |
nightly_retention, gdpr_subject_erasure, or org_data_erasure |
counts |
{events, usage_records, audit_log, digests_invalidated, …} |
notes |
Optional free-text context (e.g. subject_id=…) |
created_at |
When the deletion ran |
Registry rows are append-only and survive even the full-org erasure that produced them. They are the canonical evidence trail for GDPR / HIPAA / SOC 2 audits.
Subject erasure (GDPR right to be forgotten)
/api/v1/org/{org_id}/subject/{subject_id}/eventsErases every event whose payload.subject_id matches. Requires the ORG_DATA.DELETE
permission. Pass dry_run=true to count matches without deleting — recommended for any
ticket-driven workflow.
# preview
curl -X DELETE "https://token.audit.id/api/v1/org/{org_id}/subject/user_42/events?dry_run=true" \
-H "Authorization: Bearer td_live_xxxx"
# execute
curl -X DELETE "https://token.audit.id/api/v1/org/{org_id}/subject/user_42/events" \
-H "Authorization: Bearer td_live_xxxx"
{
"dry_run": false,
"subject_id": "user_42",
"events_found": 184,
"events_deleted": 184,
"digests_invalidated": 3
}
The endpoint also marks any Merkle digest whose window overlapped the erased events as
invalidated_at with reason gdpr_subject_erasure. The original digest rows remain in the
table — they are flagged, not deleted — so the audit trail of what was once provable is
preserved even after the underlying events are gone. See Event signatures
for what an invalidated digest means for batch verification.
Full org erasure
/api/v1/org/{org_id}/dataCascading erasure of the entire organization — events, usage records, audit log, retention
config, the org row itself. Requires the owner role plus the ORG_DATA.DELETE permission.
The caller must echo the org slug or id in confirm_org; a mismatch returns 400 before any
delete happens.
curl -X DELETE https://token.audit.id/api/v1/org/{org_id}/data \
-H "Authorization: Bearer td_live_xxxx" \
-H "Content-Type: application/json" \
-d '{
"confirm_org": "acme-corp",
"notes": "Account closure ticket #4218"
}'
{
"ok": true,
"deleted": {
"events": 184290,
"usage_records": 184290,
"audit_log": 8421,
"organizations": 1
}
}
Aggregated summaries
Daily and monthly cost / token summaries are kept indefinitely regardless of
events_retention_days. Only raw per-event payloads age out — dashboards and management
reports continue to render history far beyond the event ledger's cutoff. The append-only
BigQuery replica honors the same window via a BQ-side table expiration policy; no separate
configuration needed.