CMP & Browser Signals
The SDK reads three standard consent signals automatically: IAB TCF, IAB GPP and Global Privacy Control. All three are on by default. You can turn any of them off for a source in Configuration.
Each reader reports one of three answers for each purpose:
granted: the signal allows the purpose.denied: the signal refuses it.no-signal: the signal says nothing either way. This is never read as consent. It simply doesn't count towards the result.
Waiting for the CMP
CMPs often load after the Surfside tag. The TCF and GPP readers wait for window.__tcfapi or window.__gpp to appear, checking every 100 ms for the first 10 seconds and then once a second. They stop after five minutes. If no CMP appears, the SDK keeps its current state: either a decision restored from an earlier page view, or the default.
When the CMP appears, the reader subscribes to it. It then reacts to every later change, including a shopper reopening the banner and changing their choice.
The readers only act on data the CMP has committed to. A CMP showing its banner, still loading, or reporting an error is ignored, so an unanswered banner is never read as a refusal.
IAB TCF v2
Reads: window.__tcfapi('addEventListener', 2, …)
The SDK acts on TCF payloads whose eventStatus is tcloaded or useractioncomplete. It ignores cmpuishown, and any payload whose cmpStatus is stub, loading or error.
TCF is the only standard signal that can deny tracking. The SDK uses two TCF purposes:
| TCF purposes | tracking | identity |
|---|---|---|
| Purpose 1 (store and/or access information on a device) refused | denied | denied |
| Purpose 1 granted, Purpose 3 (create profiles for personalised advertising) refused | granted | denied |
| Purposes 1 and 3 both granted | granted | granted |
gdprApplies is false | no-signal | no-signal |
purpose.consents missing or empty | no-signal | no-signal |
A few details:
- Publisher consents narrow the result. If the payload's
publisher.consentsblock grants at least one purpose, a purpose only counts as granted when both the purpose consent and the publisher consent grant it. - An empty block means no decision yet. TCF v2.2 requires
purpose.consentson every payload, so a CMP that has only drawn its banner publishes it empty. A block that is filled in but refuses everything is treated as a genuine refusal. - Vendor consent is not read. The SDK uses purpose-level consent only. It does not look up a vendor consent bit.
IAB GPP (US sections)
Reads: window.__gpp('addEventListener', …) and window.__gpp('ping', …)
The SDK acts on GPP data once the CMP reports signalStatus: 'ready'. It uses applicableSections to decide which sections apply to this shopper and reads only those. If applicableSections is [-1] ("no section applies"), GPP reports no-signal.
GPP only ever affects identity.
Supported sections
The SDK reads the US National section and every US state section:
| ID | Section | ID | Section | ID | Section |
|---|---|---|---|---|---|
| 7 | usnat | 14 | usmt | 21 | usnj |
| 8 | usca | 15 | usor | 22 | ustn |
| 9 | usva | 16 | ustx | 23 | usmn |
| 10 | usco | 17 | usde | 24 | usmd |
| 11 | usut | 18 | usia | 25 | usin |
| 12 | usct | 19 | usne | 26 | usky |
| 13 | usfl | 20 | usnh | 27 | usri |
New US sections that IAB adds later are also honoured, as long as the CMP's payload makes the section unambiguous. Non-US sections (EU TCF, Canadian TCF, and the deprecated US Privacy string) are not read by the GPP reader.
How a section is read
The opt-out fields use the GPP encoding: 0 = not applicable, 1 = opted out, 2 = did not opt out.
| What the applicable sections contain | identity |
|---|---|
Any of SaleOptOut, SharingOptOut or TargetedAdvertisingOptOut is 1 | denied |
The section's GPC sub-section (or Gpc field) is true | denied |
MspaServiceProviderMode is 1 | denied |
None of the above, and at least one opt-out field is 2 | granted |
Only 0 values, or no opt-out fields | no-signal |
If several sections apply, any one opting out is enough to deny.
SaleOptOut is the only field every US section defines. SharingOptOut exists only in usnat and usca. Every other state uses TargetedAdvertisingOptOut, which usca does not define. A field that a state's section doesn't define is ignored, so a mock or CMP configuration that uses the wrong field reads as no-signal, not as a denial.
MspaCoveredTransaction is recorded but doesn't change the decision. The SDK does not currently act on GPP sensitive-data fields (such as SensitiveDataProcessing). If your CMP collects those and you need them to restrict identity, pass the result to setConsent.
Jurisdictions
The sections that applied are recorded as jurisdictions (for example ["usca"]) on the consent state and on every event, even when the shopper didn't opt out. Which state's law applies is useful context in itself. At most two are recorded; when more apply, state sections are kept ahead of usnat. Every applicable section still counts towards the decision.
Global Privacy Control
Reads: navigator.globalPrivacyControl
| Value | identity |
|---|---|
true | denied |
false or absent | no-signal |
GPC only ever affects identity. Collection continues with identity removed.
Some browser extensions set GPC a moment after the page loads. If GPC isn't present at first, the SDK keeps checking for it for up to five minutes and applies the opt-out as soon as it appears.
Within a page view GPC works in one direction only: it can switch on, but it won't switch off. If a shopper turns GPC off, their earlier GPC opt-out is lifted only once the SDK has confirmed GPC is really absent, which means a page view where GPC stays absent for the full five-minute check.
When a signal goes quiet
A CMP that hasn't loaded yet looks the same as a CMP with nothing to report. So that a slow CMP can't accidentally reverse a shopper's earlier opt-out:
- A
no-signalfrom TCF or GPP does not clear an earlier denial from that same source. Only the CMP publishing an actual grant can lift it. - A
no-signalfrom GPC (once the SDK has confirmed GPC is absent) does clear an earlier GPC denial, because the shopper really has turned it off. - A new
setConsentcall always replaces your previoussetConsentdecision for the purposes you pass.