Skip to content

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.

POST /sites/:siteId/gdpr/export
Authorization: Bearer <admin-token-or-admin-api-key>
Content-Type: application/json

Body — 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.

POST /sites/:siteId/gdpr/delete
Authorization: Bearer <admin-token-or-admin-api-key>
Content-Type: application/json

Same 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, and identity_links row 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.
TableCovered?Notes
eventsYesEvery row with the matching anon_id or user_id.
sessionsYesEvery session for the subject.
user_profilesYesThe profile row, if user_id was supplied or resolves through identity_links.
identity_linksYesAll anon ↔ user mappings touching the subject.
audit_logNoAudit 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.
dlqPartialDLQ 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).
BackupsNoThe platform does not delete from your Postgres backups. Operator policy: rotate backups within the GDPR-mandated window, or document the policy in your DPIA.

The platform resolves subjects in two ways:

  • user_id — joins identity_links to find every anon_id ever bound to the user, then deletes across the union.
  • anon_id — direct match on the anon_id column.

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.

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.

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.