Sourcepoint consent recipe
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.
Option 1: read consent in onConsentReady
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.
// 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.
Option 2: custom JavaScript consent and reject actions
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:
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
- Action settings: leave "On status change only" off for both actions so saved decisions run again on later pages, and leave "When new user" off for the reject action, so the reject action, and the saved refusal it records, never runs for a visitor who has not chosen yet (an unknown choice stays held). See Sourcepoint's Consent Actions and Reject Actions articles for each setting. Match the actions' geographic scope to your policy.
- AMP pages cannot run Sourcepoint's custom JavaScript actions. Do not rely on this recipe there.
- GDPR Standard (not TCF) campaigns: read the vendor with _sp_.gdpr.getCustomVendorConsents(callback) instead of __tcfapi('getCustomVendorConsents', 2, callback), and check the vendor and every required purpose. Register your handlers through _sp_.config.events (onConsentReady and the other event callbacks) before Sourcepoint loads, as in Option 1, and give each snapshot a revision so an older callback never overrides a newer decision.
- Withdraw synchronously when a refusal arrives; do not wait for a snapshot first.
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.