GDPR endpoints
GDPR Article 15 (right of access) and Article 17 (right to erasure) are
implemented as two query API endpoints. Both are admin-only or
admin-scoped api_key.
Export
Section titled “Export”POST /sites/:siteId/gdpr/exportAuthorization: Bearer <admin-token-or-admin-api-key>Content-Type: application/jsonBody — exactly one of subject.anon_id or subject.user_id:
{ "subject": { "user_id": "u_42" } }{ "subject": { "anon_id": "anon_abc123" } }Supplying both is 400 invalid_subject. Body cap: 16 KB.
Response (200):
{ "site_id": "site_marketing", "subject": { "user_id": "u_42" }, "counts": { "events": 12, "sessions": 3, "user_profiles": 1, "identity_links": 2 }, "events": [/* full row dumps */], "sessions": [/* … */], "user_profiles": [/* … */], "identity_links": [/* … */]}The response is the entire row contents from each affected table —
including the events.raw JSONB column, which carries the verbatim wire
payload. Hand it to the data subject as their Article 15 export.
Delete (RTBF)
Section titled “Delete (RTBF)”POST /sites/:siteId/gdpr/deleteAuthorization: Bearer <admin-token-or-admin-api-key>Content-Type: application/jsonSame subject body shape:
{ "subject": { "user_id": "u_42" } }Response (200):
{ "site_id": "site_marketing", "subject": { "user_id": "u_42" }, "counts": { "events": 12, "sessions": 3, "user_profiles": 1, "identity_links": 2 }}Behaviour:
- Hard-deletes every
events,sessions,user_profiles, andidentity_linksrow tied to the subject inside one transaction. - Idempotent — replaying against an already-purged subject returns zero
counts (still
200). - Recorded in the audit log with the actor and the SHA-256-truncated subject id.
What gets covered
Section titled “What gets covered”| Table | Covered? | Notes |
|---|---|---|
events | Yes | Every row with the matching anon_id or user_id. |
sessions | Yes | Every session for the subject. |
user_profiles | Yes | The profile row, if user_id was supplied or resolves through identity_links. |
identity_links | Yes | All anon ↔ user mappings touching the subject. |
audit_log | No | Audit rows are append-only; the subject id in the audit row is SHA-256-truncated rather than stored verbatim, so erasing the subject does not require deleting audit history. |
dlq | Partial | DLQ rows tagged with the subject id are deleted; rows where the subject is buried in the payload JSON are not (the operator must inspect manually if dlq retention is long). |
| Backups | No | The platform does not delete from your Postgres backups. Operator policy: rotate backups within the GDPR-mandated window, or document the policy in your DPIA. |
Subject resolution
Section titled “Subject resolution”The platform resolves subjects in two ways:
user_id— joinsidentity_linksto find everyanon_idever bound to the user, then deletes across the union.anon_id— direct match on theanon_idcolumn.
If a single human ever appeared as both an anon and a known user, supplying
user_id is the right move — it picks up their pre-identification trail
through identity_links.
Performance
Section titled “Performance”Both endpoints are O(rows-per-subject) and run inside one transaction for delete. For very high-volume users (think tens of thousands of events) the delete can take a few hundred milliseconds; the API responds when the transaction commits. The endpoints are not a hot path — operator-tier volume only.
Every gdpr.export and gdpr.delete call is recorded in
audit_log with the actor (admin or api_key id)
and a SHA-256-truncated copy of the subject id. The truncation prevents
the audit row from being its own GDPR violation while still leaving an
audit trail.
Compliance notes
Section titled “Compliance notes”This page is technical documentation. It is not legal advice. Your DPIA, the legal basis for processing, the retention policy, and the rest of the GDPR surface are operator responsibilities. Leatsmap provides the mechanism; you supply the policy.