Docs

Guide: showing a user their own history

Let a signed-in person see what they agreed to and when, by calling the server-side history endpoints from your backend.

A “Your privacy choices” page that lists what someone agreed to, when, and how they changed their mind builds trust — and answers the most common data-access question without a ticket. The history is already in the vault; you only need to read it and show it.

There is no public (`pk_`) history endpoint, by design: a browser key would let anyone who copies it read someone’s history. The pattern is your backend calling the sk_ endpoints for the user who is logged in, then sending the browser only what you want it to show. For a complete, formal copy of everything held about a person, use a data-access request instead (Vault & data requests).

The two endpoints

EndpointReturnsFilter
GET /v1/subjects/{ref}/consent/historyEvery consent record, newest first (max 500)?purpose=<key>
GET /v1/subjects/{ref}/preferences/historyEvery preference write, newest first (max 500)?field=<key>

Both need a secret (sk_) key — with the subjects:read scope, if you have restricted the key’s scopes — and {ref} is the person’s identifier, for example external_id:u_9241 (URL-encode it). Reading never creates a subject: an unknown one is a 404.

Call it from your backend

curl -H "Authorization: Bearer $TC_SECRET_KEY" \
  "https://api.tripticonsent.tripticode.com/v1/subjects/external_id%3Au_9241/consent/history?purpose=marketing_email"
import { TripticonsentClient } from '@tripticonsent/sdk';

const tc = new TripticonsentClient({
  baseUrl: 'https://api.tripticonsent.tripticode.com',
  apiKey: process.env.TC_SECRET_KEY!, // sk_live_… — server only
});

// e.g. inside GET /api/my-privacy-history, after you authenticate the request
export async function myHistory(userId: string) {
  const subject = 'external_id:' + userId; // NEVER take this from the request
  const [consent, prefs] = await Promise.all([
    tc.getConsentHistory(subject),
    tc.getPreferenceHistory(subject),
  ]);
  return { consent, prefs }; // …or map them to just the fields your UI shows
}
Build the subject from your own session (the logged-in user’s id), never from a value the browser sends. Otherwise any user could ask for anyone’s history.

What comes back

Consent history: { subjectId, count, legalBasis, records }. legalBasis maps each purpose key to its basis. Each record has:

  • purposeKey, purposeVersion, state (GRANTED, DENIED, WITHDRAWN or system-written EXPIRED)
  • collectedAt, expiresAt, channel, locale, pageUrl
  • source — API (the person acted), IMPORT / MIGRATION (you asserted it), or CONSOLE (written by the system, such as an expiry)
  • metadata — whatever you sent with the write, returned as-is
  • proof — the hash of the exact statement the record was given under; supersedesId — the record it replaced

Preference history: { subjectId, count, records }, each record with fieldKey, value, templateVersion, collectedAt, source, channel, pageUrl, supersedesId and confirmationState. Pending double opt-in requests are included — confirmationState is PENDING until the person clicks the link (then CONFIRMED, or EXPIRED / CANCELLED) — so filter or label them if you only want confirmed choices.

What is not returned

  • IP addresses and user agents — they stay in the vault and in the data-access export. The double opt-in proof in preference history is trimmed to its non-personal fields (doubleOptIn, confirmationId, requestedAt, method).
  • Other people’s data — a history is scoped to one subject (and follows merges into it).
  • Anything beyond the newest 500 rows per call; filter by purpose or field for long histories.
  • Receipt ids — history rows don’t carry them. Keep the receiptId returned when you recorded the consent if you want to offer a downloadable proof (GET /v1/receipts/{id}).
metadata is returned verbatim, so only put in it what you would be comfortable showing back to the person — or don’t pass it through to your UI. Show source in plain words (“You chose this on the cookie banner” / “Recorded by Acme from your existing contract”), and translate purposeKey into the purpose’s own title with GET /v1/purposes.