Skip to main content

JavaScript Scripts and embeds

Vendor consent

A visitor allows marketing, then switches off X Pixel. Other marketing scripts load; X Pixel does not. c15t loads a script, network request or iframe that names a vendor only when both are true:

  • Its category is allowed.
  • The visitor has not switched its vendor off.

A vendor switch never grants a category. With marketing denied, X Pixel stays blocked whatever its own switch says. You do not need IAB TCF for this. Under an iab policy, c15t ignores vendor slugs and takes vendor consent from the TC string instead; see IAB TCF.

Declare the vendors and record a switch

Built-in @c15t/integrations helpers already list their vendor by name, so you only declare a vendor for a script of your own, or to replace a helper's name and privacy policy.

With createConsentRuntime, declare the vendors, then render the switch in your own UI with the headless runtime. The stock preference dialog that init() from @c15t/browser mounts lists vendor switches on its own. This module declares X Pixel, reads whether it may run, and records a new switch:

src/vendor-consent.ts
import { isVendorAllowed } from 'c15t';
import type { SaveResult, Vendor } from 'c15t';
import type { ConsentRuntime } from 'c15t/runtime';

export const vendors: Vendor[] = [
	{
		category: 'marketing',
		id: 'x-pixel',
		name: 'X Pixel',
		privacyPolicyUrl: 'https://x.com/privacy',
	},
];

// Whether the vendor may run now: it is declared, its category is allowed
// and the visitor has not switched it off. An undeclared id returns false.
export const canVendorRun = function canVendorRun(
	runtime: ConsentRuntime,
	vendorId: string
): boolean {
	return isVendorAllowed(runtime.kernel.getSnapshot(), vendorId);
};

// Record one vendor switch. The empty category input confirms no category;
// `save()` with no input would also confirm every displayed category.
export const saveVendorSwitch = function saveVendorSwitch(
	runtime: ConsentRuntime,
	vendorId: string,
	granted: boolean
): Promise<SaveResult> {
	return runtime.kernel.commands.save({}, { vendors: { [vendorId]: granted } });
};

Pass vendors to the runtime next to scripts:

src/consent-runtime.ts (partial)
import { vendors } from './vendor-consent';

export const runtime = createConsentRuntime({
	mode: hosted({ backendURL: 'https://your-project.inth.app' }),
	scripts,
	vendors,
});

init() from @c15t/browser accepts the same vendors option. Its client exposes the runtime as client.runtime, so the functions above work with it too.

isVendorAllowed(snapshot, id) from c15t backs canVendorRun().

A vendor reads as allowed only when it is declared, its category is allowed and the visitor has not switched it off. An id that nothing declares, in vendors, on a script or iframe, or from the backend, reads as not allowed. A typo such as x-pixle or a vendor you forgot to declare never looks like consent. In development, c15t logs a console warning that names the id.

Call saveVendorSwitch(runtime, 'x-pixel', false) from your switch's change handler. To confirm categories and vendors in one action, pass both to the kernel's save:

await runtime.kernel.commands.save({
	marketing: true,
	measurement: true,
	vendors: { 'x-pixel': false },
});

To stage vendor switches next to category switches and save them together, use a preference draft: draft.setVendor(id, granted) stages one, and draft.reset() drops them. runtime.kernel.getSnapshot().vendors?.declared lists the declared vendors, and vendorChoice holds the recorded decision.

Vendor fields

FieldRequiredBehavior
idYesLowercase slug of up to 64 characters: letters, digits, ., _ and -.
nameYesName shown in the preference dialog.
categoryYesA category, or a condition such as { or: ['measurement', 'marketing'] }.
privacyPolicyUrlYesLink shown next to the vendor.
description, legalName, homepageUrlNoExtra detail shown on the vendor card.
disabledNoList the vendor without a switch. A stored denial for it no longer applies.

The id must match the vendor slug on the script. Every @c15t/integrations helper sets vendor to its script ID, so xPixel() is x-pixel, gtag() is gtag and cloudflareZaraz() is cloudflare-zaraz. Each vendor guide names its slug.

You don't need to declare a helper's vendor. Each helper also sets vendorDetails on its script: the vendor's name, privacy policy, homepage and legal entity. The dialog lists the vendor with its own switch from those. Declare the vendor in vendors to replace them, for example to link your own data processing notice, or when you self-host a tool such as Matomo, Umami, Plausible or PostHog and the vendor's privacy policy doesn't cover your install. A declaration replaces vendorDetails as a whole and doesn't merge with it.

Your own scripts can set vendorDetails too. A script with a vendor slug and no name and privacyPolicyUrl from any source still loads with its category, but the dialog has no switch for it. In development, c15t logs a console warning that names the slug. Production builds skip the warning.

A self-hosted backend can declare vendors too. /init returns them and c15t merges them with the vendors in code. When both declare the same id, the code declaration wins.

Gate scripts, requests and iframes by vendor

Put the vendor slug on each target:

TargetField
Script configurationvendor: 'x-pixel'
Network blocker rulevendor: 'x-pixel'
Iframe blockerdata-vendor="x-pixel" on the <iframe>

An iframe with data-vendor and no data-category is gated on the vendor alone. A script with alwaysLoad still loads when its vendor is off. Its callbacks receive info.vendor.granted, so the SDK can stop itself. The Google Consent Mode, RudderStack and Cloudflare Zaraz helpers do this for you. While their vendor is off, they send every optional category as denied.

Iframe blocker and network blocker cover those targets.

What c15t stores

c15t stores only the vendors a visitor switched off, and sends the full vendor map to the backend with the consent record. Under an opt-out policy, every vendor starts on. Turning a vendor off that was on reloads the page, the same as withdrawing a category, so code the vendor already ran stops.

What switching a vendor off does not do

  • It does not delete cookies the vendor already set. Clear on revocation does not run, because the category stays allowed. Delete the vendor's cookies from the script's onConsentChange when info.vendor.granted is false.
  • It does not expire. The switch stays off until the visitor changes it or uses Accept All or Reject All, even across a policy change.
  • Adding a vendor does not ask returning visitors again. A new vendor starts on inside an allowed category. Change the policy's copyRevision if a new vendor should prompt again.

Test the production build in a private window with DevTools Network open and filtered to ads-twitter.com:

  1. Register xPixel() and declare x-pixel. Allow marketing. uwt.js loads.
  2. Switch X Pixel off in your control. c15t reloads the page. After the reload there is no uwt.js request, and other marketing scripts still load.
  3. Reload again. canVendorRun(runtime, 'x-pixel') still returns false.
  4. Record Accept all with runtime.kernel.commands.save('all'). uwt.js loads again.