Sourcepoint consent recipe

View as Markdown

Start the SDK paused until consent, then grant or withdraw from Sourcepoint's consent state for a vendor you create for Telltale. This recipe uses Sourcepoint's public web API: the onConsentReady event callback in window._sp_ configuration, the getCustomVendorConsents command for GDPR (TCF) and window._sp_.usnat.getUserConsents for US national privacy. A second option uses Sourcepoint's custom JavaScript consent and reject actions.

Set up the vendor

In your Sourcepoint vendor list, add Telltale as a custom vendor and note its vendor ID. Use the same vendor in each regulation's vendor list that applies to your site.

Add the handler to the window._sp_ configuration that already loads Sourcepoint. If you have other event callbacks, keep them and call apply from your existing onConsentReady.

JavaScript
// Runs before the Sourcepoint script; the SDK tag must be loaded first.
const client = window.Telltale.init({
  endpoint: 'https://app.trytelltale.dev',
  key: 'YOUR_SITE_PUBLISHABLE_KEY',
  waitForConsent: true,
  discover: true
});
const VENDOR_ID = 'YOUR_SOURCEPOINT_CUSTOM_VENDOR_ID';
const decisions = {};                     // latest vendor decision per applicable regulation on this page
const revisions = { gdpr: 0, usnat: 0 };  // per regulation: a newer event supersedes its own slower snapshot
const decide = (regulation, allowed) => {
  if (allowed === undefined && !(regulation in decisions)) return; // no decision for this vendor yet: stay held
  // A vendor that vanishes from the snapshot after a decision is a withdrawal.
  decisions[regulation] = allowed === true;
  // Collection needs every decided regulation to allow it, so one regulation's withdrawal is never overridden by another's grant.
  const allowedEverywhere = Object.values(decisions).every(Boolean);
  (allowedEverywhere ? client.optIn() : client.optOut()).catch(console.error);
};

// Runs fn(true) as soon as check() passes (at once when it already does), or fn(false) after 10 seconds. Reads only.
const whenReady = (check, fn) => {
  const started = Date.now();
  (function wait() {
    if (check()) return fn(true);
    if (Date.now() - started > 10000) return fn(false);
    setTimeout(wait, 100);
  })();
};

window._sp_queue = window._sp_queue || [];
window._sp_ = window._sp_ || { config: {} };
window._sp_.config.events = window._sp_.config.events || {};
window._sp_.config.events.onConsentReady = function (messageType, consentUUID, euconsent, info) {
  // Not applicable: decide by your own legal policy. It takes no revision, so it never supersedes another snapshot.
  if (!info?.applies || !Object.hasOwn(revisions, messageType)) return;
  const current = ++revisions[messageType];
  if (messageType === 'gdpr') {
    // The TCF API can lag behind the event on slow pages: wait for it (/adapters/v1/cmp.js readiness) instead of throwing.
    whenReady(() => typeof window.__tcfapi === 'function', ok => {
      if (!ok) return current === revisions.gdpr && decide('gdpr', undefined); // no snapshot: held, or a withdrawal after a decision
      window.__tcfapi('getCustomVendorConsents', 2, (data, success) => {
        if (current !== revisions.gdpr) return; // an older GDPR snapshot never overrides a newer GDPR decision
        const grant = success ? data?.grants?.[VENDOR_ID] : undefined; // a failed snapshot after a decision withdraws
        decide('gdpr', grant ? grant.vendorGrant === true : undefined);
      });
    });
  } else {
    whenReady(() => typeof window._sp_?.usnat?.getUserConsents === 'function', ok => {
      if (!ok) return current === revisions.usnat && decide('usnat', undefined);
      window._sp_.usnat.getUserConsents((consents, success) => {
        if (current !== revisions.usnat) return;
        const vendor = success ? consents?.vendors?.find(v => v._id === VENDOR_ID) : undefined;
        decide('usnat', vendor ? vendor.consented === true : undefined);
      });
    });
  }
};

whenReady only reads: it waits for Sourcepoint's TCF or US national API to exist before asking it for a snapshot, and if the API never appears within 10 seconds the event is treated like a failed snapshot (held before a decision, a withdrawal after one). It never clicks a message and never changes a choice.

onConsentReady fires when the consent object is ready on page load and again after the user makes or changes a choice, so it covers both the initial state and withdrawal. vendorGrant is true only when the vendor and its required purposes are granted. Snapshot callbacks are asynchronous, so each applicable event takes a revision for its own regulation and a slower, older callback for that regulation is ignored. Events for another regulation or a non-applicable event never supersede it, so a withdrawal is never lost. With GDPR and US campaigns on one page, collection runs only while every regulation that has decided allows it. Once a decision has been applied, a failed snapshot or a vendor missing from it withdraws rather than leaving an earlier approval in force. With the page-owned bridge from the Consent guide, delete the init() call and call window.telltaleConsent.applyConsent(true or false) from decide.

Sourcepoint can run custom JavaScript when a vendor or purpose is consented (a consent action) or rejected (a reject action). Expose two functions from a same-origin file and reference them from window:

JavaScript
const client = window.Telltale.init({
  endpoint: 'https://app.trytelltale.dev',
  key: 'YOUR_SITE_PUBLISHABLE_KEY',
  waitForConsent: true,
  discover: true
});
window.telltaleSourcepoint = {
  grant: () => client.optIn().catch(console.error),
  withdraw: () => client.optOut().catch(console.error)
};

Set the vendor's consent action to window.telltaleSourcepoint.grant() and its reject action to window.telltaleSourcepoint.withdraw(). Load this file before Sourcepoint so either action can run before or after the SDK finishes loading. With the page-owned bridge from the Consent guide, use window.telltaleConsent.applyConsent(true) and applyConsent(false) as the two actions instead (and drop window.telltaleSourcepoint). Configure the vendor's purposes with consent as the legal basis: a consent action can also run for legitimate interest. Leave "on status change only" off so the saved decision is reapplied on every page view. Sourcepoint notes that custom JavaScript actions run in the parent page, so your CSP must allow them; avoid any setup that needs unsafe-inline.

Notes

Withdrawal calls optOut(), which stops owned collection and tools and discards pending evidence. It cannot recall data already sent. Logging in never grants consent. Keep identity (identify/reset) separate from this consent bridge.