---
title: Vendor consent
description: Let visitors allow a category such as marketing on a plain HTML
  page and still turn off one vendor in it, with the vendors option,
  data-c15t-vendor on gated tags and the vendor switches in the c15t script
  tag's preference dialog.
group: frameworks
lastModified: "2026-10-10T16:01:45+01:00"
---
## How vendor consent works

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](./iab).

## Declare the vendors

Queue a `config` call with `vendors` before the c15t script tag. Each `id` is
the slug your gated tags and iframes name:

```html title="index.html"
<!-- Runs before the deferred c15t.js tag, so init() picks it up -->
<script>
	window.c15t = window.c15t || [];
	c15t.push([
		'config',
		{
			vendors: [
				{
					id: 'posthog',
					name: 'PostHog',
					category: 'measurement',
					description: 'Product analytics and session insights.',
					privacyPolicyUrl: 'https://posthog.com/privacy',
				},
				{
					id: 'youtube',
					name: 'YouTube',
					category: 'measurement',
					description: 'Embedded videos.',
					privacyPolicyUrl: 'https://policies.google.com/privacy',
				},
				{
					id: 'x-pixel',
					name: 'X Pixel',
					category: 'marketing',
					description: 'Ad conversion tracking.',
					privacyPolicyUrl: 'https://x.com/en/privacy',
				},
			],
		},
	]);
</script>
```

`vendors` is an ordinary `config` option, so `c15t.init({ vendors })` on a
page with `data-manual` works too. See
[configuration](/docs/frameworks/html/configuration).

## Vendor fields

|Field|Required|Behavior|
|--|--|--|
|`id`|Yes|Lowercase slug of up to 64 characters: letters, digits, `.`, `_` and `-`.|
|`name`|Yes|Name shown in the preference dialog.|
|`category`|Yes|A category, or a condition such as `{ or: ['measurement', 'marketing'] }`.|
|`privacyPolicyUrl`|Yes|Link shown next to the vendor.|
|`description`, `legalName`, `homepageUrl`|No|Extra detail shown on the vendor card.|
|`disabled`|No|List 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](/docs/integrations/overview) 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 tags and iframes by vendor

Add `data-c15t-vendor` to a gated `<script type="text/plain">` tag, next to
its `data-c15t-category`. The tag runs once its category is allowed and the
visitor has not switched its vendor off:

```html title="index.html"
<!-- PostHog's snippet, saved as a file on your site -->
<script
	type="text/plain"
	data-c15t-category="measurement"
	data-c15t-vendor="posthog"
	src="/vendor/posthog.js"
></script>
<!-- X Pixel's base code, pasted inline -->
<script
	type="text/plain"
	data-c15t-category="marketing"
	data-c15t-vendor="x-pixel"
>
	!function(e,t,n,s,u,a){e.twq||(s=e.twq=function(){s.exe?s.exe.apply(s,arguments):s.queue.push(arguments);
	},s.version='1.1',s.queue=[],u=t.createElement(n),u.async=!0,u.src='https://static.ads-twitter.com/uwt.js',
	a=t.getElementsByTagName(n)[0],a.parentNode.insertBefore(u,a))}(window,document,'script');
	twq('config','YOUR_X_PIXEL_ID');
</script>
```

Add `data-vendor` to a gated iframe:

```html title="index.html"
<iframe
	data-src="https://www.youtube-nocookie.com/embed/czTksCF6X8Y?playsinline=1"
	data-category="measurement"
	data-vendor="youtube"
	title="YouTube video"
	allow="encrypted-media; picture-in-picture"
	allowfullscreen
></iframe>
```

|Target|Field|
|--|--|
|Gated `<script type="text/plain">` tag|`data-c15t-vendor="x-pixel"` next to `data-c15t-category`|
|Gated iframe|`data-vendor="x-pixel"`|
|`scripts` entry in `config`|`vendor: 'x-pixel'`|
|Network blocker rule|`vendor: 'x-pixel'`|

`data-c15t-vendor` does nothing without `data-c15t-category`. On a page whose
c15t tag has a nonce, a vendor-gated tag still needs that nonce, like every
[gated tag](/docs/frameworks/html/components/gated-script#gated-tags-on-a-page-with-a-nonce).
An iframe with `data-vendor` and no `data-category` is gated on the vendor
alone.

A tag's `data-c15t-vendor` slug counts as declared, so a stored denial holds
even before your `vendors` list or the backend names that vendor. The
preference dialog lists the vendor only once a declaration gives it a `name`
and a `privacyPolicyUrl`.

## What the preference dialog shows

The preference dialog in `c15t.js` lists each declared vendor under its
category, with its own switch. `c15t.headless.js` has no dialog; record vendor
switches from your own UI with `save()`.

The dialog lists only vendors that have a `name` and a `privacyPolicyUrl`.
A vendor's switch is disabled while its category is off. Save records the
vendors the visitor changed. Accept All and Reject All clear every vendor
denial, so each vendor follows its category again. The labels come from
`consentManagerDialog.vendors` in the translations: `title`,
`privacyPolicy`, `disabledByCategory` and `switchLabel`.

## Read and record vendor choices from your own script

`window.c15t` and `c15t.client` have three reads for vendor consent:

|Method|Returns|
|--|--|
|`getDeclaredVendors()`|The vendors from `vendors`, the backend and the slugs on scripts, gated tags and iframes. Empty under an IAB policy.|
|`getVendorChoice()`|The recorded vendor decision, whose `denied` lists the vendors the visitor switched off, or `null` before any vendor decision.|
|`isVendorAllowed(id)`|`true` when the vendor is declared, its category is allowed and the visitor has not switched it off.|

Reads cannot be queued, so call them from `onInit` or after `c15t.ready()`.

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.

`save()` accepts a `vendors` map next to the categories. Vendors you leave out
keep their recorded state. `save` can be queued, so this works before and
after the tag loads:

```html
<script>
  window.c15t = window.c15t || [];
  c15t.push(['save', { marketing: true, vendors: { 'x-pixel': false } }]);
</script>
```

Call it from a visitor's action, such as a switch in your own form, never at
page load. A save that changes only vendors still fires the `consent` event,
and c15t checks gated tags again, so a tag waiting on a vendor that was
switched back on runs. See [window.c15t API](/docs/frameworks/html/api) and
[events and callbacks](/docs/frameworks/html/callbacks).

## 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](./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.

## Verify vendor consent

Open the page in a private window with DevTools Network open and filtered to
`ads-twitter.com`:

1. Declare `x-pixel` and gate X Pixel's tag with
   `data-c15t-category="marketing"` and `data-c15t-vendor="x-pixel"`. Open
   **Privacy settings**. The marketing row lists X Pixel with a switch.
2. Allow marketing and save. `uwt.js` loads.
3. Open **Privacy settings**, switch X Pixel off and save. c15t reloads the
   page. After the reload there is no `uwt.js` request, and other marketing
   tags still run.
4. Reload again. The X Pixel switch is still off, and in the console
   `c15t.isVendorAllowed('x-pixel')` returns `false`.
5. Turn marketing off. The X Pixel switch is disabled.
6. Open **Privacy settings** and click **Accept All**. `uwt.js` loads again.
