Technical audit · 08.09.2026

postimees.ee

Estonia's largest news portal

Estonia's largest news portal — a single capture of the homepage, 4262 requests, 66 nodes. The disclosure here is exemplary: the Didomi consent platform under the IAB TCF standard and a named partner list of about 950 thousand characters — Google, Adform, Criteo, RTB House, Cxense, Chartbeat and hundreds of others are named. But the user's decision is not protected by this disclosure. The user refused: the TCF consent string encodes zero purposes and zero legitimate interest. Despite the refusal, immediately afterwards a full advertising auction starts — over two hundred requests to Google Ad Manager, Setupad, Criteo, RTB House, Adform, DoubleClick — and each carries the partners the same 'no consent' string. And even before the choice, in the first seconds, the Cxense audience data platform already profiles the visitor by a stored identifier, Chartbeat sends a pageview with a token, and the Google ad server loads before the banner itself. GA4 runs with no consent signal at all.

Request timeline

+190 ms · the ad server before the CMP
pagead2.googlesyndication.com/tag/js/gpt.js — the Google Publisher Tag loads straight from the markup, at +190 ms, before the SDK of the Didomi consent platform (+212 ms). In parallel, Setupad (stpd.cloud), Chartbeat (static.chartbeat.com), Confiant (npttech.com) and Weborama (static.wwcloud.eu, +247 ms) load.
+652…+1167 ms · the Cxense audience data platform
cdn.cxense.com/cx.cce.js and cx.js, then api.cxense.com/segment/lookup at +1167 ms — a request for advertising segments with a persistent identifier persisted=af85f1f8…. Profiling for targeting, relying on reading a stored identifier, before any choice.
+1350…+1609 ms · analytics and measurement
www.googletagmanager.com (+1350 ms); pong.chartbeat.net/ping with a user token (+1437 ms); tag.aticdn.net/piano-analytics.js (+1478 ms); xqnzlnc.pa-cd.com/event with idclient=OPT-OUT (+1609 ms — no identifier).
~+57 min · the ad auction AFTER the refusal
Right after the refusal is recorded, programmatic starts: Setupad header bidding (+3443870 ms), Criteo (static.criteo.net, gum.criteo.com), RTB House (creativecdn), Google DoubleClick (+3444559 ms), safeframe and tpc.googlesyndication, euddn (le303 +3597127 ms), Adform (+3697796 ms), a Kanal2 node. Over two hundred advertising requests, each carrying the string gdpr_consent=CQqOSAA… — 'no consent'.
~+60 min · GA4 and Universal Analytics
region1.analytics.google.com/g/collect — a GA4 page_view (G-380QQP0TRP) with the status gcs=-, and a request to www.google-analytics.com. The analytics runs with no consent signal.

Declared versus actual

✓ Controller — Postimees Grupp; data-protection terms with an emphasis on legitimate interest (õigustatud huvi — 33 mentions) — declared
✓ Consent management — the Didomi platform under the IAB TCF standard (CMP ID 7); a banner with the choice 'Olen nõus' / 'Halda valikuid' — declared
✓ The necessary categories are declared on the basis of legitimate interest, including journalistic-content-consumption measurement — declared
✓ The partner list is disclosed by name and exhaustively (an IAB TCF vendor list of ~950 thousand characters): Google, Adform, Criteo, RTB House, Weborama, Piano, Cxense, Setupad, Xandr, Chartbeat and hundreds of others — declared
✓ Supervisory authority — Andmekaitse Inspektsioon — declared
+ That the ad auction starts under a recorded refusal (the consent string encodes zero purposes), passing the partners a 'no consent' signal — not declared
+ That the Cxense audience data platform profiles by a persistent identifier before the choice — not declared
+ That the Google Publisher Tag ad server, Chartbeat and Weborama fire before the choice — not declared
+ That GA4 runs with no consent signal (gcs=-) — not declared

Transfer timings

+190 ms pagead2.googlesyndication.com

Google Publisher Tag (gpt.js) from the markup, before the Didomi SDK.

+1167 ms api.cxense.com

segment/lookup with a persistent identifier persisted=af85f1f8…. Advertising audience segments.

+1437 ms pong.chartbeat.net

pageview /ping with a user token.

+3443371 ms sdk.privacy-center.org (Didomi)

consent.given on click; the TCF string CQqOSAA… = zero purposes, zero legitimate interest.

+3443507 ms pagead2.googlesyndication.com

gdpr_consent=CQqOSAA… (the zero string) goes to Google Ad Manager; the auction starts after the refusal.

+3444193 ms gum.criteo.com

Criteo receives the zero-consent string.

+3444559 ms ad.doubleclick.net

DoubleClick receives the zero-consent string.

+3614812 ms region1.analytics.google.com

GA4 page_view, G-380QQP0TRP, consent mode not configured.

Detected trackers

Show data recipients (22)
  • Didomi (sdk.privacy-center.org — 2829 req., api.privacy-center.org) — IAB TCF consent management platform (CMP ID 7); banner 'Olen nõus' / 'Halda valikuid'. The user's recorded choice is a refusal (the consent string encodes zero purposes and zero legitimate interest)
  • Cxense / Piano DMP (cdn.cxense.com, api.cxense.com, comcluster.cxense.com) — audience data platform; api.cxense.com/segment/lookup carries a persistent identifier and requests advertising segments at +1167 ms — before the choice
  • Chartbeat (pong.chartbeat.net — 231 req., static.chartbeat.com, mab.chartbeat.com) — publisher analytics; pong/ping with a user token at +1437 ms — before the choice
  • Google Publisher Tag / Ad Manager (pagead2.googlesyndication.com — 237 req.) — ad server; gpt.js at +190 ms (before the CMP itself), /pagead/ping at +2526 ms — before the choice; the full auction — afterwards, under a recorded refusal
  • Weborama (static.wwcloud.eu — 12 req.) — audience data platform; loads at +247 ms — before the choice
  • Confiant (www.npttech.com) — ad quality and safety verification; +213 ms — before the choice
  • Piano Analytics / AT Internet (tag.aticdn.net, xqnzlnc.pa-cd.com — 26 req., api-esp.piano.io) — content-consumption measurement; pa-cd.com/event before the choice carries idclient=OPT-OUT (no identifier)
  • Google Tag Manager (www.googletagmanager.com — 20 req.) — tag container; +1350 ms — before the choice
  • Google Analytics 4 (region1.analytics.google.com, G-380QQP0TRP) — runs with the status gcs=- (consent mode not configured); Universal Analytics (www.google-analytics.com) — legacy counter
  • Setupad (node.setupad.com — 28 req., wb.setupad.com, stpd.cloud) — header-bidding platform; the auction starts AFTER the refusal
  • Google DoubleClick (ad.doubleclick.net, stats.g.doubleclick.net, s0.2mdn.net, tpc.googlesyndication.com, *.safeframe.googlesyndication.com) — ad delivery and display; AFTER the refusal, carrying the zero-consent string
  • Google Ad Traffic Quality (ep1.adtrafficquality.google, ep2.adtrafficquality.google) — ad-traffic quality control; AFTER the refusal
  • Criteo (static.criteo.net, gum.criteo.com) — advertising network / retargeting (RTB); AFTER the refusal, receives the zero-consent string
  • RTB House (invstatic101.creativecdn.com) — advertising network (RTB); AFTER the refusal
  • Adform (track.adform.net) — advertising network (RTB); AFTER the refusal
  • euddn.net (le303.euddn.net — 29 req., router.euddn.net, ode601.euddn.net, ode602.euddn.net) — advertising delivery infrastructure (RTB); AFTER the refusal
  • Adobe (animate.adobe.com) — ad-creative animation library; AFTER the refusal
  • Kanal2 (tigu.kanal2.ee) — a node of the Estonian TV channel (media group); AFTER the refusal
  • city24 (static.img-city24.ee) — a node of a sister site of the media group; AFTER the refusal
  • Libraries and fonts (cdnjs.cloudflare.com, cdn.jsdelivr.net, code.jquery.com, cdn.ampproject.org, fonts.googleapis.com, fonts.gstatic.com, www.gstatic.com, www.google.com, www.google.ee) — functional delivery
  • First party (www.postimees.ee, v.postimees.ee, vx.postimees.ee, services.postimees.ee, auth.postimees.ee, c2/infographics/ams/andy/cdn.postimees.ee, f.pmo.ee and f7–f12/f302.pmo.ee, sisuturundus.pmo.ee) — Postimees Grupp infrastructure
  • Partner list — an IAB TCF vendor list of ~950 thousand characters; Google, Adform, Criteo, RTB House, Weborama, Piano, Cxense, Setupad, Xandr, Chartbeat and hundreds of others are named

Indicators of GDPR non-compliance

Context

postimees.ee is Estonia’s largest news portal; the controller is Postimees Grupp. Consent is managed by the Didomi platform under the IAB TCF standard. The capture is a single recording of the homepage: 4262 requests, 66 nodes, a long session during which the user explored the consent settings and ultimately refused.

This audit stands apart twice over. First, the disclosure of recipients here is done at an exemplary level: the partner list is laid out by name and in full — an IAB TCF vendor list of about 950 thousand characters, where Google, Adform, Criteo, RTB House, Cxense, Chartbeat, Setupad and hundreds of others are named. Second, it is precisely this completeness that makes the main discrepancy plain: disclosing everyone is not the same as respecting the user’s decision.

Who receives data directly

Before the choice — Cxense / Piano (audience data, profiling), Chartbeat (analytics), Google Publisher Tag (ad server), Weborama (audience data), Confiant (ad verification), Google Tag Manager. After the recorded refusal — a full advertising auction: Setupad, Criteo, RTB House, Adform, euddn, Google DoubleClick, plus GA4 and Universal Analytics. Piano Analytics measurement before the choice goes out without an identifier (OPT-OUT).

Declared vs. actual

The discrepancy here is not in the set of recipients but in the attitude to the user’s decision — and that is fundamental.

First — the disclosure is exemplary. The Didomi consent platform under the IAB TCF standard, a banner with the choice ‘Olen nõus’ / ‘Halda valikuid’, and a named partner list of ~950 thousand characters. As for who was named, there is no complaint against the site: it named practically everyone.

Second — but the refusal is ignored. The user made a choice, and Didomi recorded the TCF consent string CQqOSAA…. This string decodes unambiguously: consent to all twenty-four processing purposes — zero, legitimate interest — zero. That is, a full refusal is recorded. Nevertheless, precisely after the refusal is recorded, a full programmatic auction starts: over two hundred requests to the Google Ad Manager ad server, Setupad header bidding, Criteo, RTB House, Adform, the euddn infrastructure, Google DoubleClick. And the requests carry the partners the same zero string — verified, gdpr_consent=CQqOSAA… goes to Google, Criteo and DoubleClick. The mechanics are these: the auction is tied to the mere fact of the decision, not to its content. It fires identically both under consent and under refusal — in the latter case merely honestly passing the partners a ’no consent’ signal. But a refusal should stop processing, not accompany it.

Third — and before the choice, profiling is already happening. Already in the first seconds, long before the decision, the Cxense audience data platform requests the visitor’s advertising segments (api.cxense.com/segment/lookup, +1167 ms), carrying a persistent identifier persisted=af85f1f8…. This is the reading of a stored device identifier and profiling for advertising — not the measurement that the banner assigns to legitimate interest. Alongside, Chartbeat sends a pageview with a user token (+1437 ms), the Google Publisher Tag ad server loads at +190 ms — before the CMP itself — and Weborama is loaded (+247 ms).

Fourth — analytics without a consent signal. GA4 (G-380QQP0TRP) sends events with the status gcs=- — Google’s consent mode is not configured.

Proven: the user clicked ‘Keeldun kõigist’ (reject all). In the Didomi ‘Nõusolekuhaldus’ panel, the button rejecting all categories, including advertising ones (Reklaamküpsised), is selected. Didomi records the decision at +3443371 ms with a consent.given event (the event type means ‘a decision was made’, not ‘consent was given’), and the recorded TCF string CQqOSAA… decodes to zero consent across all twenty-four purposes and zero legitimate interest — that is, a machine-confirmed refusal.

Proven: the ad auction starts after the refusal. Right after the decision is recorded (+3443383 ms onwards), over two hundred advertising requests go out — Google Ad Manager, Setupad, Criteo, RTB House, Adform, euddn, DoubleClick.

Proven: the partners receive the zero string. gdpr_consent=CQqOSAA… is present in the requests to Google Ad Manager, Criteo and DoubleClick — that is, a ’no consent’ signal is passed, but the requests still happen.

Proven: trackers run before the choice. Cxense segment/lookup with a stored identifier — at +1167 ms; Chartbeat pong/ping with a token — at +1437 ms; Google Publisher Tag — at +190 ms; Weborama — at +247 ms.

Proven: part of the stack respects the refusal. The Piano Analytics collector sends idclient=OPT-OUT, without an identifier; against this backdrop the ad auction that starts under a refusal stands out especially plainly.

Not proven, and not reproduced: the content of the segments and the full identifier. Which advertising segments exactly Cxense returned and what the full stored identifier is are not reproduced in the audit; the conclusion rests on the fact of a request with a stored identifier and on the fact of the advertising requests after the refusal.

Observation boundaries

The capture covers the homepage and a long session during which the user explored the settings and refused; browser behaviour is recorded, not the services’ internal workings. The TCF consent string is decoded from the gdpr_consent parameter that the site passes to the partners; the type of decision (consent.given) is read from the Didomi events. The attribution of requests to recipients relies on domains and URL schemes. The Cxense persistent identifier is presented in the audit in truncated form. How each advertising partner handles the received zero string on its side — whether it respects the refusal server-side — is not verified by a browser-level capture; what matters is that the requests to them after the refusal took place.

Separately, on reading consent: the Didomi platform, on any choice — both acceptance and refusal — sends an event of type consent.given (‘a decision was recorded’). Therefore an automated check that keys on the event name may mistakenly show ‘consent given’; the actual state is read not from the event name but from the TCF string, which here encodes a full refusal.

The file is published redacted of personal data: cookie values in requests have been removed, and stored identifiers in the evidence bodies are truncated. Response headers, statuses, the load order, initiators, the moment the decision is recorded and the TCF consent string are preserved and verified directly against the published file.

The legal assessment is made by the competent authority — Andmekaitse Inspektsioon.

Conclusion

The country’s largest news portal approached disclosure in an exemplary way: the Didomi consent platform under the IAB TCF standard and a named partner list of almost a million characters, where practically all recipients are named. To the question ‘who processes the data’ the site answers more fully than most. But the user’s decision is not protected by this disclosure.

The user refused — the TCF consent string encodes zero purposes and zero legitimate interest. Despite the refusal, immediately afterwards a full advertising auction starts: over two hundred requests to Google Ad Manager, Setupad, Criteo, RTB House, Adform, DoubleClick — and each carries the partners the same ’no consent’ string. The mechanism is tied to the fact of the decision, not to its content: it fires under a refusal as well. And even before the choice, in the first seconds, the Cxense audience data platform profiles the visitor by a stored identifier, Chartbeat sends a pageview with a token, the Google ad server loads before the banner, and GA4 runs with no consent signal at all.

Tellingly, part of the stack behaves correctly: the Piano Analytics collector sends OPT-OUT, without an identifier, before the choice. This makes the discrepancy especially plain — where measurement manages without an identifier, the ad auction starts right on top of the refusal. Exhaustive disclosure of recipients is necessary but not sufficient: it replaces neither holding processing back until the choice nor respecting the refusal after it.

Remediation: stop the ad auction on a refusal — condition the launch of header bidding and requests to Google Ad Manager, Criteo, RTB House, Adform, euddn and DoubleClick on the actual content of the decision, not on the mere fact of its being recorded; condition the Cxense segment request and any reading of stored identifiers before the choice on consent; condition the loading and pings of the Google Publisher Tag, Chartbeat and Weborama on consent; configure Google’s consent mode so that GA4 passes a consent signal (currently gcs=-); and reconsider assigning advertising audience profiling to legitimate interest.

Evidence

How to verify: download the file and compute its checksum — sha256sum file.har (Linux, macOS) or certutil -hashfile file.har SHA256 (Windows); it must match the one shown. The conclusions apply to the scope of this measurement — see the limitations of the method.

Original (audit)
HAR file: ee/postimees-ee-2026-09-08.har
SHA-256: 9b5dedce72b3411d8de72aabdd9d4fbec18b8af3f7b5c03c16fd74ccefc0b404
Re-check snapshot
Awaiting changes
HAR files are stored on EU infrastructure (Proton Drive). SHA-256 is published for integrity verification.