Setting Consent from Your Site
If your site runs an IAB TCF or GPP CMP, the SDK reads it automatically and you may not need anything on this page. Use the consent API when:
- you have a custom consent banner, or a CMP configured with its own categories rather than IAB signals
- you need to apply a decision the standard signals don't carry, such as an account-level privacy setting
- you want to read the current consent state, for example to decide whether to show a banner
There are three commands: setConsent, getConsent and getConsentSignal.
Where you can call them
All three work before the tracker is created. Consent belongs to the browser, not to a tracker, so you don't need a tracker namespace (setConsent:digital isn't needed) and you don't need to wait for newTracker.
- wave.js tag
- edge.js snippet
- npm SDK
The wave.js tag creates window.surf when it runs. Because it loads asynchronously, your CMP may answer first. Wrap your calls so they wait for the tag:
function surfConsent(command, argument) {
if (typeof window.surf === 'function') {
window.surf(command, argument);
return;
}
setTimeout(function () { surfConsent(command, argument); }, 100);
}
surfConsent('setConsent', { tracking: true, identity: false });
surf stub with wave.jswave.js only loads the rest of the SDK if window.surf doesn't exist yet. If your page defines window.surf itself before wave.js runs, the SDK will not load.
The Analytics Tag snippet defines window.surf straight away. Call surf(…) anywhere after the snippet, including before newTracker:
surf('setConsent', { tracking: true, identity: false });
Import the commands from @surfside/surfside-events. They return their result directly, and you can call them before newTracker:
import { setConsent, getConsent, getConsentSignal } from '@surfside/surfside-events';
setConsent({ tracking: true, identity: false });
const state = getConsent();
@surfside/surfside-eventssetUserId, setUserIdFromLocation, setUserIdFromReferrer, setUserIdFromCookie, enableAnonymousTracking and disableAnonymousTracking exist in both packages. Only the versions in @surfside/surfside-events respect consent. The ones in @surfside/browser-sdk set identifiers with no consent check, so events could carry a user ID while their consent context says identity was denied.
setConsent
Records your site's decision for one or both purposes.
surf('setConsent', { tracking: true, identity: true }); // shopper accepted
surf('setConsent', { tracking: true, identity: false }); // opted out of sale / targeted ads
surf('setConsent', { tracking: false, identity: false }); // rejected all
surf('setConsent', { identity: false }); // tracking left as it was
| Field | Type | Description |
|---|---|---|
tracking | boolean | true grants, false denies. If you leave it out, your previous tracking decision stays as it was. |
identity | boolean | true grants, false denies. If you leave it out, your previous identity decision stays as it was. |
What to know:
- It's remembered. The decision is stored on the device and restored on later page views, so call it when the shopper decides, not on every page.
- The most restrictive source wins.
setConsentis one source alongside TCF, GPP and GPC. A grant fromsetConsentcannot lift a denial from any of them. A denial fromsetConsentalways takes effect. - Purposes are independent.
{ tracking: false }doesn't denyidentityon its own. If the shopper rejected everything, pass both. - It works while signal readers are turned off. Your own
setConsentdecisions apply even if Surfside has turned the consent framework off for your source.
getConsent
Returns the combined decision from every source.
- wave.js tag
- edge.js snippet
- npm SDK
surfConsent('getConsent', function (state) {
console.log(state);
});
surf('getConsent', function (state) {
console.log(state);
});
const state = getConsent();
With the surf() queue, return values are discarded, so pass a callback to receive the state.
{
tracking: 'granted', // 'granted' | 'denied'
identity: 'denied', // 'granted' | 'denied'
signal: 'gpp', // the source that decided: 'tcf' | 'gpp' | 'gpc' | 'api' | 'none'
jurisdictions: ['usca'], // US GPP sections that apply, when GPP reported them
updatedAt: '2026-10-13T14:03:11.402Z' // when the decision last changed
}
signal: 'none' means nothing has expressed a preference and both purposes are at their default (granted).
getConsentSignal
Returns what one source reported, before it was combined with the others. Use it to find out why the combined decision is what it is.
surf('getConsentSignal', {
source: 'gpp', // 'tcf' | 'gpp' | 'gpc' | 'api'
callback: function (signal) {
console.log(signal);
}
});
It takes a single object. With the surf() queue only the first argument reaches the command, so the callback has to go inside it.
{
source: 'gpp',
tracking: 'no-signal', // 'granted' | 'denied' | 'no-signal'
identity: 'denied',
jurisdictions: [{ section: 'usca', sectionId: 8, saleOptOut: 'opted-out', sharingOptOut: 'opted-out' }],
signalString: 'DBABBg~BUoAAABA.QA', // the CMP's TC or GPP string, when there is one
cmpId: 300
}
undefined means that source has never reported: its reader is turned off, or it is still waiting for the CMP to appear. That's different from a source that reported no-signal.
Connecting a custom banner
Call setConsent from each button, and once when the page loads if your banner has already stored a choice:
document.querySelector('#accept-all').addEventListener('click', function () {
surf('setConsent', { tracking: true, identity: true });
});
document.querySelector('#reject-all').addEventListener('click', function () {
surf('setConsent', { tracking: false, identity: false });
});
document.querySelector('#do-not-sell').addEventListener('click', function () {
surf('setConsent', { identity: false });
});
Connecting a CMP with custom categories
If your CMP uses its own categories rather than IAB signals, map them onto the two purposes in its change callback. The example below uses OneTrust's default category IDs; check yours in your OneTrust configuration.
function syncSurfsideConsent() {
var groups = window.OnetrustActiveGroups || '';
surf('setConsent', {
tracking: groups.indexOf('C0002') !== -1, // Performance cookies
identity: groups.indexOf('C0004') !== -1 // Targeting cookies
});
}
window.OptanonWrapper = function () {
syncSurfsideConsent();
OneTrust.OnConsentChanged(syncSurfsideConsent);
};
The same pattern works for any CMP: read the shopper's choice, then call setConsent with the two booleans.
If your CMP also publishes TCF or GPP, the SDK already reads it. Calling setConsent as well is safe, but remember that the most restrictive answer wins: a denial from either one is enforced.
Opt-in sites
The SDK's default is granted, which suits US opt-out regimes. If your site needs a decision before anything identifying is collected, set a denial as early as possible and lift it when the shopper accepts:
// As early as possible on every page, before the shopper has chosen
if (!shopperHasChosen()) {
surf('setConsent', { tracking: false, identity: false });
}
// When the shopper accepts
surf('setConsent', { tracking: true, identity: true });
Because a setConsent denial can only be lifted by setConsent, a CMP grant arriving through TCF won't override it. Make sure your accept path calls setConsent.
With tracking denied, the SDK keeps sending events, without cookie IDs and with the IP anonymised at the collector. If your legal basis means no event may be sent before consent, don't load the Surfside tag until the shopper accepts.
Logging out vs withdrawing consent
These two are different:
| Situation | Call | Effect |
|---|---|---|
| The shopper withdraws consent | surf('setConsent', { identity: false }) | Identity is removed and future identity calls are held. Remembered across page views. |
| The shopper logs out | surf('removeUser') | The current shopper's identity is removed and anything held for them is discarded. Consent is unchanged, so the next shopper to log in can be identified. |
See What Happens When Consent Is Denied for the full detail.