---
title: Vendor consent
description: Let visitors allow a category such as marketing on an Astro site
  and still turn off one vendor in it, with the vendors option on the c15t()
  integration and the page client from c15t/astro/client.
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

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.

Add `vendors` to the `c15t()` integration in `astro.config.mjs`. Each entry
uses the `vendor` slug its script already carries:

```ts title="astro.config.mjs (partial)"
vendors: [
	{
		category: 'measurement',
		description: 'Product analytics and session insights.',
		id: 'posthog',
		name: 'PostHog',
		privacyPolicyUrl: 'https://posthog.com/privacy',
	},
	{
		category: 'measurement',
		description: 'Embedded videos.',
		id: 'youtube',
		name: 'YouTube',
		privacyPolicyUrl: 'https://policies.google.com/privacy',
	},
	{
		category: 'marketing',
		description: 'Ad conversion tracking.',
		id: 'x-pixel',
		name: 'X Pixel',
		privacyPolicyUrl: 'https://x.com/en/privacy',
	},
],
```

Vendor entries are plain data, so they survive the JSON serialization the
integration options go through and can sit in `astro.config.mjs` next to
`mode`. The server also asks about the categories these vendors sit in, so a
vendor in `marketing` puts marketing in the banner decision and the
preference dialog.

## 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 scripts, requests and iframes by vendor

Put the vendor slug on each target:

|Target|Field|
|--|--|
|Script configuration|`vendor: 'x-pixel'`|
|Network blocker rule|`vendor: 'x-pixel'`|
|Iframe blocker|`data-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.

On an Astro site, two more targets take a vendor slug:

|Target|Field|
|--|--|
|Inert inline script|`data-c15t-vendor="x-pixel"` next to `data-c15t-category`|
|Your own custom element|`client.isVendorAllowed('youtube')` in its render function|

An inert tag with `data-c15t-vendor` runs once its category is allowed and the
visitor has not switched the vendor off:

```astro title="src/pages/index.astro (partial)"
<script
  is:inline
  type="text/plain"
  data-c15t-category="marketing"
  data-c15t-vendor="x-pixel"
>
  // X Pixel's base code
</script>
```

`data-c15t-vendor` does nothing without `data-c15t-category`. Under a
nonce-based Content Security Policy, the tag still needs the page's nonce. See
[gate an inline script](/docs/frameworks/astro/scripts#gate-an-inline-script).
[Embeds](/docs/frameworks/astro/embeds) covers the iframe blocker's
`data-vendor` attribute and the custom element pattern.

## What the preference dialog shows

The preference dialog lists each declared vendor under its category, with its
own switch, whichever `ui` framework renders the dialog island: React, Vue or
Svelte.

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 in your own script

The page client from `getConsentClient()` in `c15t/astro/client` reads and
records vendor consent:

|Member|Returns or does|
|--|--|
|`client.getDeclaredVendors()`|The vendors from `vendors`, the backend manifest and the slugs on scripts and iframes. Empty under an IAB policy.|
|`client.getVendorChoice()`|The recorded vendor decision, whose `denied` lists the vendors the visitor switched off, or `null` before any vendor decision.|
|`client.isVendorAllowed(id)`|`true` when the vendor is declared, its category is allowed and the visitor has not switched it off.|
|`client.save({ ...categories, vendors })`|Saves the categories you pass and the vendor grants in `vendors`. Vendors you leave out keep their recorded state.|

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.

The [video component](/docs/frameworks/astro/embeds#gate-an-embed-with-a-custom-element)
reads `client.isVendorAllowed('youtube')`, so it needs `youtube` in `vendors`.

Call `save()` only from a visitor's action, such as a switch in your own
form. This records marketing as allowed and X Pixel as off:

```ts title="src/scripts/x-pixel-switch.ts (partial)"
import { getConsentClient } from 'c15t/astro/client';

await getConsentClient()?.save({
	marketing: true,
	vendors: { 'x-pixel': false },
});
```

[Client API](/docs/frameworks/astro/client-api#save-consent) covers the rest
of the page client.

## 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

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

1. Register `xPixel()`. 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
   scripts still load.
4. Reload again. The X Pixel switch is still off.
5. Turn marketing off. The X Pixel switch is disabled.
6. Open Privacy settings and click Accept All. `uwt.js` loads again.
