# Consent records

Consent stores each visitor decision as a signed server record. You can review statistics, export records as CSV, or retrieve a decision chain using the record id from a visitor's cookie.

Records are retained for 1095 days. Their contents and retention can support your records of consent, but do not by themselves establish that you meet a particular legal obligation.

## What a record contains

| Field | Meaning |
|---|---|
| `recordId` | Identifier of this decision |
| `necessary`, `preferences`, `statistics`, `marketing` | Category flags |
| `method` | `explicit`, `implied` or `withdrawal` |
| `ver` | Decision format version |
| `utc` | Server timestamp of the decision |
| `region` | Two-letter country code, or empty |
| `page_url` | Page origin and path |
| `stamp` | Cryptographic signature over the canonical fields |
| `providers_rev` | Provider-set versions disclosed for the decision |
| `replaces_id` | Previous record in the decision chain, if any |

Records contain no name, email, IP address, visitor id, user agent or device fingerprint.

### The stamp

Each product has a signing key. Consent uses it to compute a truncated HMAC-SHA-256 over a canonical set of fields. The stamp can establish whether those fields have changed since signing; it does not attest to fields outside that set.

### The page URL keeps no query string

The banner removes the query string and fragment before sending `page_url`, and the server strips them again before storage. Only origin and path remain, avoiding query parameters such as `?email=` or `?token=` in the record.

### `necessary` is always true

The API rejects `necessary: false`, and the database enforces the same rule. The banner's necessary category cannot be declined.

### Withdrawal is a record, not a deletion

Withdrawing creates a record with `method: withdrawal` and `replaces_id` pointing to the previous choice. Other choice changes also link to the record they replace, so the chain preserves the decision history.

An unknown `replaces_id`, or one belonging to another product, is stored as empty. Links cannot cross product boundaries.

## Statistics

Use `consent_stats_get` or the app's reports tab to read decisions over a window. The response includes:

- `total`: the number of records.
- `accept_all_pct`, `deny_all_pct` and `custom_pct`: the percentage in each choice group.
- `by_category`: percentages for each optional category.
- `range.from` and `range.to`: the exact bounds used by the query.

Supply the window as `Nd`, such as `30d` or `90d`. Invalid formats return an error. The server's default window is 30 days and its default maximum is 365 days.

The optional flags determine the group: all three true means `accept_all`, all false means `deny_all`, and a mixture means `custom`. A withdrawal with all optional flags false therefore counts in `deny_all`.

Percentages are rounded independently to whole numbers and can sum to 99, 100 or 101. With no records, `total` and all percentages are zero. Use the returned `range.from` and `range.to` for an export that matches the statistics window.

`consent_dashboard_stats_get` provides the smaller summary used when the dashboard opens.

## Recent decisions

`consent_dashboard_decisions_list` returns up to ten recent decisions, newest first. The server labels each row `withdrawal` when that is its method; otherwise it uses `accept`, `deny` or `custom` from the flags. The app and MCP receive the same labels.

## Export

Both export paths require **Admin** access. They read the same records:

| Path | Result |
|---|---|
| `GET /consent/dashboard/export` | Streaming CSV, using a server-side cursor rather than buffering the full export |
| `consent_records_export` | Inline CSV: 1000 rows by default, at most 10000 |

When more records match than the inline limit allows, it returns `truncated: true`, the full `total_count`, and a `download_url` for the streaming endpoint with the same filters.

The CSV columns are fixed in this order:

```text
decision_id, stamp, necessary, preferences, statistics, marketing,
method, ver, utc, region, page_url, created_at, replaces_id
```

Booleans use `true` and `false`, timestamps use ISO-8601, and missing values produce empty cells. Visitor-influenced values beginning with `=`, `+`, `-`, `@` or a control character receive a quote prefix to prevent spreadsheet formula execution.

Both paths accept inclusive `from` and `to` bounds in strict ISO-8601 format. An inverted range is rejected.

For one visitor's history, pass the record id from their consent cookie as `record_id` to the inline export. It follows the decision chain within your product; an id from another product returns an empty chain. This export covers Consent records only. Other information you hold about that person must be handled separately when answering an access request.

A successful export emits `consent.records.exported` with the actor, surface and row count. A client-aborted download does not emit a successful-export event.

## Retention

| Item | Lifetime | Effect |
|---|---|---|
| Consent cookie | 12 months | Expiry prompts the visitor to choose again |
| Consent record | 1095 days from creation | A daily job deletes expired records per product |
| Retired signing key | 1125 days from retirement | Keeps signing keys available beyond record retention |

These periods are implemented defaults and are not configurable. The 1095-day record period was chosen for consent accountability; it is not a universal GDPR duration. Your own retention requirements depend on your processing and jurisdiction.

The cleanup job never removes active keys, and the database prevents deletion of a key still referenced by a record.

## Erasure

Consent's erasure operation is product-wide. It deletes the product's Consent tables in one transaction, following their foreign-key dependencies, with no Consent tombstone. Organization-wide erasure resolves into a request for each product.

End-user erasure cannot select Consent rows by a person's identity because no such identifier is stored. A visitor can instead locate their decision chain with the record id in their cookie. See [Data and privacy](/privacy/data-and-privacy) for the wider export and erasure model.

## Next steps

- [The consent tools](/privacy/tools): check permissions before reading or exporting records.
- [Data and privacy](/privacy/data-and-privacy): understand other data and retention rules.
