Consent · engineers · PMs · marketers
Withdrawing consent has to hit the same URL that accepted it
Accept is a pixel, a collect ping, or a CAPI post. Withdraw is supposed to be the same class of artifact with a different payload, or no artifact at all. A modal that now says rejected while facebook.com/tr still carries last hour's yes is a UI change. It is not a revoke.
Accept is a request. Withdraw has to be one too
The grant path is already instrumented. A CMP callback expands gdpr_consent, gtag consent update runs, Meta gets an fbp write, Floodlight fires with gdpr=1 and a TC String. QA pastes that URL. The miss is the other direction. GDPR says consent has to be as easy to withdraw as to give. That sentence is about the right, not about whether your GTM container has a second tag that fires on reject. If the only thing that changes on withdraw is the banner copy, the vendors that received the yes never heard the no.
A 2025 audit of Tranco top-200 sites treated revocation as its own object, not as the inverse screenshot of accept. Of 158 sites that could be checked at the interface, 32 offered revoke through a substantially different surface than accept, and 33 required two or more steps to find revoke while accept took zero. Four of those sites stored advertising or analytics cookies and offered no revocation at all. That is the UX half. The wire half is worse, and it is the half a pixel log can actually see.
Cookies that keep processing
Withdraw is not complete if advertising and analytics cookies survive. The same audit then counted those cookies after revoke. On 69 of 120 sites (57.5 percent), the cookies were still there. The banner had a new state. The browser still held identifiers that were written on the basis of the old state. Downstream pixels that read those cookies keep processing. A Network tab that only watches the first accept will never see this, because the leak is the absence of a delete.
QA that stops at refuse-on-landing is the wrong test for a returning user. The sequence is accept, confirm the hop, revoke, confirm the hop again. After revoke, facebook.com/tr, a Floodlight activity, and /g/collect should be gone, redacted, or carrying a string that no longer encodes yes. If they are identical to the accept capture, withdraw did not travel. Pixellint will not delete the cookies for you. It will tell you whether the URL you captured after revoke is still a granted IAB hop.
https://www.facebook.com/tr?id=1234567890123456&ev=PageView&noscript=1
A TC String that still says yes
TCF sites store consent in the string, in CMP cookies, and on the network. The 2025 audit looked at 281 sites running TCF. On 22 of 136 where they could read the TCF string after revoke, the stored string was still a positive consent. That is not a missing parameter. That is a yes that outlived the user's no. Core privacy rules will decode whatever you paste. If you paste the post-revoke URL and the first six bits still read version 2 with purposes granted, the vendor that received that hop received a grant. The banner is not evidence against the bits.
gdpr_consent=1 after revoke is the same bug as gdpr_consent=1 after accept: it is not a TC String. A real string that still encodes grant is a different bug. Do not celebrate that the alphabet looks like TCF. Decode it. pixellint validate url on the fired request is the check. Template macros that still hold [GDPR_CONSENT_123] after serve are a miss of a third kind. Empty and unexpanded are template exemptions. A populated grant after revoke is not.
https://ad.doubleclick.net/ddm/activity/src=1234567;type=convr0;cat=purch0;ord=8675309;gdpr=1;gdpr_consent=CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA?
Third parties that never hear no
The same TCF crawl asked a simpler network question: did every party that was told about accept also get a request that told them about revoke. On 101 of 136 sites (74 percent in that subsample), third parties that had received the yes were not informed of the withdraw. The CMP can be correct in __tcfapi and still leave the pixels that fired on grant running on the old string. Vendors do not poll the CMP. They see the next HTTP request, or they do not.
That is why a revoke that only updates first-party CMP cookies is incomplete. Floodlight, Meta, LinkedIn, TikTok, and CAPI each need their own signal or their own hold. Consent Mode update on gtag does not write gdpr_consent. dpo=LDU does not write a TC String. A server event queued at accept and retried after revoke is a new processing unless you gated it. Store the CMP choice with the session. Skip the postback, or send a redacted event, when the basis is gone.
- Capture the fired pixel URL after accept and after revoke. They must differ, or the revoke hop must be absent.
- Decode gdpr_consent on both captures. A grant string after revoke is a fail even if the banner flipped.
- Repeat for CAPI JSON: event_name, em, fbp, fbc, client_ip_address. A Purchase with match keys after refuse is the banner losing to the webhook.
- Redirect chains: if hop two drops the new string, hop three is still the old grant. See the live hop, not the template.
Bids after opt-out are a second oracle
A 2024 PETS study used advertiser bidding as the compliance check after CMP opt-out, not the banner. They filtered Alexa top-100k down to 352 sites that ran Didomi, Quantcast, OneTrust, or Cookiebot plus Prebid, under both GDPR and CCPA. After Cookiebot opt-out, personas still drew higher bids than the control under both regimes (Shopping under CCPA matched control). Bid behavior did not significantly change between Cookiebot opt-out and opt-in. Didomi moved bids more. Quantcast and NAI often did not. The point is not a league table of CMPs. The point is that a recorded opt-out is compatible with the auction still pricing the profile.
You cannot paste a bid into Pixellint and get a lawful-basis ruling. You can still refuse to treat CMP-state-granted as the only artifact. If Prebid is on the page, the bid responses after revoke are part of the same question as the pixel URL. If they look like the accept capture, withdraw did not reach demand. That is an ads problem and a consent problem on the same wire.
Lint the hop after revoke
The existing consent docs cover accept: blocking before first paint, a real TC String, GPP, no duplicate keys, CAPI still needing a basis. This page is the withdraw pass. Same tools, later in the session. Fresh profile, accept, paste. Revoke, paste again. Server-side GTM and CAPI get the second paste too. A 200 from Graph API after revoke means Meta accepted JSON. It does not mean you had a basis to send it.
pixellint validate url on the live Floodlight or vendor hop. pixellint validate json on the CAPI body. Core will flag gdpr_consent_missing, malformed strings, version bits that are not 2, duplicate_signal. It will not flag illegal processing, and it will not decide lawful basis. Independent of IAB Europe and of the audits cited here. If the post-revoke capture is still a grant, the fix is the CMP callback, the ad-server macro, or the webhook gate, not a second banner.
{
"data": [{
"event_name": "Purchase",
"event_time": 1770000000,
"action_source": "website",
"user_data": {
"em": ["shopper@example.com"],
"fbp": "fb.1.1770000000.123",
"client_ip_address": "203.0.113.42"
}
}]
}
Check the artifact
Paste the pixel URL or JSON body into the
playground. Same engine as
pixellint validate. Nothing leaves the tab.