A CMP upgrade leaves one consent string on the page. Engineering copies it into every ad-server macro that mentions privacy. The impression pixel fires. Network tab shows a long base64 blob on gpp. QA signs off.
The vendor reading GPP decodes the first six bits of the header and stops. Those bits are 2, not 3. The string is a valid TCF v2 TC String. It is not a GPP string.
https://example.com/px?id=1&gpp=CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA&gpp_sid=2
Nothing in that URL looks broken if you only check that gpp is URL-safe base64. The mix-up is structural, not cosmetic.
Two parameters, two encodings
IAB privacy signals are not interchangeable keys for the same value.
TCF v2 puts the TC String in gdpr_consent (or a Floodlight path equivalent) alongside gdpr=1. The first six bits of the core segment are the TC String version, fixed at 2 for v2.
GPP puts a composite string in gpp, with sections joined by tildes (~). The first section is a header whose first six bits are fixed at 3 as the GPP Header field. The next six bits are the GPP specification version (currently 1). A well-formed v1 header often starts with the letters DB.
One CMP callback can produce both outputs. They belong on different query keys:
https://example.com/px?id=1
&gdpr=1
&gdpr_consent=CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA
&gpp=DBACNYA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA~1YNN
&gpp_sid=2
Copying gdpr_consent into gpp because both are long base64 strings is the usual post-migration bug. It reads thorough in a ticket screenshot. On the wire it tells the GPP decoder it received a TC String in the wrong slot.
The GPP docs on pixellint.org walk through header type 3, version 1, and what happens when a TC String lands in gpp. The TCF v2 page covers the mirror mistake: gdpr_consent=1 passes a character check and is not consent because the version bits do not read 2.
What gpp_sid is for
gpp_sid carries the section ID or IDs in force for this transaction. Normally one integer, at most two comma-separated. It is not a table of contents for every section encoded in the string.
The GPP spec does not require gpp_sid to match every section inside gpp. A string can encode more sections than the callee applies on this hop. gpp_sid says which section applies now.
During a US Privacy cutover you may still send deprecated us_privacy alongside GPP. That is two generations of US signal, not a duplicate of the same key. Duplicate consent parameters on one URL (two gpp keys, or gpp plus a second copy of the same TC String in gdpr_consent with conflicting bits) are a different failure mode than putting one string in the wrong parameter.
IAB deprecated the standalone US Privacy string on 31 January 2024 in favor of GPP. Keeping us_privacy=1YNN while you roll out GPP is a documented migration path. Dropping it before every partner reads gpp_sid looks like you went silent in California.
How you catch it
Character-set checks will not save you. Both TC Strings and GPP strings are URL-safe base64. You need to decode the header bits.
I maintain Pixellint, an open source linter for pixels and conversion API payloads. The core privacy pack decodes those first six bits on gpp and gdpr_consent. On a TC String pasted into gpp, it prints:
$ pixellint validate url 'https://example.com/px?id=1&gpp=CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA&gpp_sid=2'
rulepack: core
error core.privacy.gpp_header_type
`gpp` is `CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA`,
whose header decodes to type 2. The GPP spec fixes the header type
to 3, so this is not a GPP string.
fix: Send the GPP string from the CMP. A TC String belongs in
`gdpr_consent`, not here.
Paste the fired pixel URL into the browser validator if you do not want a local install. Nothing you paste leaves the browser.
Fix the CMP-to-ad-server mapping, not the alphabet. The TC String is fine. It belongs in gdpr_consent. The GPP string from the same CMP belongs in gpp with a matching gpp_sid.
Unexpanded macros are a separate class. A URL that still holds ${GPP} never sent GPP. Empty gpp= is an unfilled template slot. Core privacy rules skip those so a creative is not scored as a malformed string before serve. After the impression fires, the live request needs a decoded header type 3 and a populated sid. Consent on the wire is the request the vendor sees, not the banner state in the page.
OpenRTB uses the same strings
OpenRTB 2.6 added regs.gpp and regs.gpp_sid alongside the older regs.us_privacy and user.consent fields. The encoding rules do not change because the container is JSON. A TC String in regs.gpp fails the same header-type check. A placeholder in user.consent fails the same version-bit check as gdpr_consent=1 on a pixel.
Programmatic and pixel teams often fix one path and leave the other. Bid requests and impression trackers should carry the same consent contract, on the keys each spec names.
Where to go next
- Consent and privacy overview maps TCF, GPP, US Privacy, and vendor-specific dialects.
-
GPP header type is 3 covers
gpp_sidshape, migration macros, and why sid does not have to list every section in the string. -
TCF v2 consent strings on pixels covers
gdpr=0vsgdpr=1, truncation, and sunset v1 strings. -
Core rulepack reference lists every
core.privacy.*finding with spec citations.
The short version: GPP header type is 3. TC String version is 2. Same CMP, two parameters, two encodings. A perfect TC String in gpp is still wrong.
Pixellint is independent of IAB Tech Lab. The rule ids above cite the GPP and TCF specs because that is where the bit layouts live, not because this is an official IAB tool.
Top comments (0)