@c15t/react 3.2.0-canary.0 → 3.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/AGENTS.md +33 -10
  2. package/README.md +5 -7
  3. package/dist/styles.tw3.css +2 -2
  4. package/dist/version.js +1 -1
  5. package/dist-types/version.d.ts +1 -1
  6. package/docs/README.md +33 -10
  7. package/docs/concepts/choose-your-setup.md +8 -9
  8. package/docs/concepts/modes.md +5 -3
  9. package/docs/customization/stylesheets.md +39 -0
  10. package/docs/frameworks/react/hooks.md +2 -1
  11. package/docs/frameworks/react/quickstart.md +3 -3
  12. package/docs/frameworks/react/upgrade-v3.md +2 -2
  13. package/docs/integrations/activecampaign.md +297 -0
  14. package/docs/integrations/ada.md +396 -0
  15. package/docs/integrations/adobe-target.md +460 -0
  16. package/docs/integrations/adroll.md +336 -0
  17. package/docs/integrations/amazon-ads.md +424 -0
  18. package/docs/integrations/appcues.md +368 -0
  19. package/docs/integrations/attentive.md +313 -0
  20. package/docs/integrations/building-integrations.md +29 -16
  21. package/docs/integrations/clearbit.md +1 -1
  22. package/docs/integrations/customer-io.md +369 -0
  23. package/docs/integrations/databuddy.md +139 -50
  24. package/docs/integrations/default-com.md +497 -0
  25. package/docs/integrations/dynamic-yield.md +373 -0
  26. package/docs/integrations/fullstory.md +298 -0
  27. package/docs/integrations/gainsight-px.md +418 -0
  28. package/docs/integrations/gorgias.md +365 -0
  29. package/docs/integrations/hubspot.md +529 -0
  30. package/docs/integrations/insider.md +550 -0
  31. package/docs/integrations/liveintent.md +327 -0
  32. package/docs/integrations/lucky-orange.md +336 -0
  33. package/docs/integrations/marketo.md +309 -0
  34. package/docs/integrations/optimizely.md +411 -0
  35. package/docs/integrations/oracle-eloqua.md +301 -0
  36. package/docs/integrations/overview.md +124 -111
  37. package/docs/integrations/pendo.md +339 -0
  38. package/docs/integrations/rb2b.md +363 -0
  39. package/docs/integrations/salesforce-pardot.md +342 -0
  40. package/docs/integrations/statable-analytics.md +345 -0
  41. package/docs/integrations/the-trade-desk.md +328 -0
  42. package/docs/integrations/vector.md +333 -0
  43. package/docs/integrations/wisepops.md +416 -0
  44. package/docs/integrations/zendesk.md +359 -0
  45. package/package.json +8 -8
  46. package/docs/integrations/other-ads-and-pixels.md +0 -94
  47. package/docs/integrations/other-analytics.md +0 -108
  48. package/docs/integrations/other-chat-and-support.md +0 -112
  49. package/docs/integrations/other-email-and-sms.md +0 -156
  50. package/docs/integrations/other-experience.md +0 -222
@@ -0,0 +1,529 @@
1
+ ---
2
+ title: HubSpot
3
+ description: Gate HubSpot's tracking code on marketing consent, forward c15t
4
+ categories and enable chat only after functionality consent.
5
+ seoTitle: HubSpot cookie consent with c15t
6
+ seoDescription: Load HubSpot after marketing consent, disable its banner and
7
+ forward consent categories. Enable chat separately and understand revocation
8
+ limits.
9
+ icon: radar
10
+ group: integrations
11
+ ---
12
+
13
+ The `hubspot` helper keeps HubSpot's tracking-code loader off the page until
14
+ marketing consent is granted. Measurement or functionality consent alone does
15
+ not load it. HubSpot can connect browsing history to CRM contacts and use
16
+ tracking data for partner profiling, so the default gates the whole loader on
17
+ marketing consent.
18
+
19
+ After consent, the helper disables HubSpot's banner and sends c15t's categories
20
+ to HubSpot before inserting the loader. c15t remains the consent interface.
21
+
22
+ ## Configure HubSpot
23
+
24
+ Copy the numeric portal ID from the filename in your HubSpot tracking snippet.
25
+ For a snippet ending in `js-eu1.hs-scripts.com/123456.js`, the `portalId` is
26
+ `123456` and the `region` is `eu1`. The portal ID is public configuration.
27
+
28
+ Remove hand-installed tracking snippets, tag-manager copies and the WordPress
29
+ plugin's tracking code before using this helper. HubSpot-hosted CMS pages
30
+ already include the tracking code. Adding this helper cannot gate requests
31
+ that another installation already sent.
32
+
33
+ | Package manager | Command |
34
+ | :-------------- | :------------------------------- |
35
+ | npm | `npm install @c15t/integrations` |
36
+ | pnpm | `pnpm add @c15t/integrations` |
37
+ | yarn | `yarn add @c15t/integrations` |
38
+ | bun | `bun add @c15t/integrations` |
39
+
40
+ ```ts title="src/consent-scripts.ts"
41
+ import { hubspot } from '@c15t/integrations/hubspot';
42
+
43
+ export const scripts = [hubspot({ portalId: '123456' })];
44
+ ```
45
+
46
+ ## Register the scripts
47
+
48
+ Complete your [framework quickstart](https://c15t.com/docs/frameworks) first. Keep its Inth
49
+ endpoint, policy, styles and consent UI. Remove the vendor's original script,
50
+ SDK initializer or tag-manager entry, so the vendor loads only through c15t.
51
+
52
+ The vendor pages put the helper in `src/consent-scripts.ts`. If your framework
53
+ quickstart already has a `scripts` array, such as the one in `c15t.config.ts`
54
+ in the Next.js guide, add the helper to that array instead of creating a
55
+ second file.
56
+ The `scripts` export is a configuration, not an initializer. Add it to the c15t provider you already have, at the registration
57
+ point for your framework below. These are edits to that provider, not a second
58
+ provider.
59
+
60
+ **Next.js**
61
+
62
+ Add the configuration to `scripts` in `c15t.config.ts`, next to
63
+ `next.config.ts`:
64
+
65
+ ```ts title="c15t.config.ts"
66
+ import { defineConsentConfig } from 'c15t/next';
67
+ import { scripts } from './src/consent-scripts';
68
+
69
+ export default defineConsentConfig({ scripts });
70
+ ```
71
+
72
+ Keep the rest of your config, such as `mode` and `routePrefix`, in the
73
+ same call. `ConsentRoot` reads the config in the browser, so the layout
74
+ keeps passing only `state`. App Router, Pages Router and static export all
75
+ read the same file. See
76
+ [Next.js scripts and embeds](https://c15t.com/docs/frameworks/next/scripts).
77
+
78
+ **TanStack Start**
79
+
80
+ Import the configuration into your root route and pass it to the existing
81
+ `ConsentRoot` as a top-level prop. Keep the loader from the
82
+ [TanStack Start quickstart](https://c15t.com/docs/frameworks/tanstack-start/quickstart):
83
+
84
+ ```tsx title="src/routes/__root.tsx"
85
+ import { scripts } from '../consent-scripts';
86
+
87
+ <ConsentRoot state={consent} scripts={scripts}>
88
+ ```
89
+
90
+ Import vendor helpers in the root route module, not in a server function.
91
+ A server function's return value must be serializable, and script
92
+ configurations carry callbacks. See
93
+ [TanStack Start scripts](https://c15t.com/docs/frameworks/tanstack-start/scripts).
94
+
95
+ **React**
96
+
97
+ Add the configuration to the existing `ConsentProvider` options, next to
98
+ `mode`:
99
+
100
+ ```tsx title="src/consent.tsx"
101
+ import { scripts } from './consent-scripts';
102
+
103
+ <ConsentProvider options={{ mode, scripts }}>
104
+ ```
105
+
106
+ `mode` is the `manifest()` value from the
107
+ [React quickstart](../frameworks/react/quickstart.md). Keep the banner,
108
+ dialog and preferences link inside the provider. See
109
+ [React scripts and embeds](../frameworks/react/scripts.md).
110
+
111
+ **Nuxt**
112
+
113
+ Register the scripts under the `c15t` key in `app/app.config.ts`. Adjust the
114
+ relative import to where you created `consent-scripts.ts`:
115
+
116
+ ```ts title="app/app.config.ts"
117
+ import { scripts } from '../src/consent-scripts';
118
+
119
+ export default defineAppConfig({
120
+ c15t: { scripts },
121
+ });
122
+ ```
123
+
124
+ The Nuxt module merges this over its options in `nuxt.config.ts` and starts
125
+ one script loader in the browser after hydration, once it has applied the
126
+ visitor's stored choice and privacy signals. Keep `scripts` out of
127
+ `nuxt.config.ts`, which reaches the browser as JSON and drops the vendor
128
+ callbacks. Write the vendor IDs into `consent-scripts.ts`. See
129
+ [Nuxt scripts and embeds](https://c15t.com/docs/frameworks/nuxt/scripts).
130
+
131
+ **Vue**
132
+
133
+ Pass the scripts to the existing `c15tVue` plugin call in `src/main.ts`:
134
+
135
+ ```ts title="src/main.ts"
136
+ import { c15tVue, manifest } from 'c15t/vue/vue-plugin';
137
+ import { scripts } from './consent-scripts';
138
+
139
+ app.use(c15tVue, {
140
+ mode: manifest(),
141
+ scripts,
142
+ });
143
+ ```
144
+
145
+ Keep your existing `mode` and other options. The plugin starts one
146
+ script loader when the app mounts, after it has applied the visitor's stored
147
+ choice. Do not also call `createScriptLoader` from a component. See
148
+ [Vue scripts and embeds](https://c15t.com/docs/frameworks/vue/scripts).
149
+
150
+ **Astro**
151
+
152
+ Add the scripts to the client entrypoint from the
153
+ [Astro quickstart](https://c15t.com/docs/frameworks/astro/quickstart), `src/c15t.client.ts`,
154
+ which the integration finds on its own. Keep `astro.config.mjs` as it is.
155
+ If the module already exports scripts, combine the two arrays.
156
+
157
+ ```ts title="src/c15t.client.ts"
158
+ import type { C15tClientOptionsExtension } from 'c15t/astro';
159
+ import { scripts } from './consent-scripts';
160
+
161
+ export default { scripts } satisfies C15tClientOptionsExtension;
162
+ ```
163
+
164
+ Vendor helpers contain callbacks, and the integration options in
165
+ `astro.config.mjs` are serialized into the page, so do not put helpers in
166
+ the integration's `scripts` option. The integration passes the client
167
+ entrypoint to the one runtime every page shares, including across
168
+ `ClientRouter` navigation.
169
+
170
+ **Svelte**
171
+
172
+ Import the scripts in the component that owns your existing provider and
173
+ pass them as a top-level prop:
174
+
175
+ ```svelte title="src/App.svelte"
176
+ <script lang="ts">
177
+ import { ConsentProvider, manifest } from 'c15t/svelte';
178
+ import { scripts } from './consent-scripts';
179
+ </script>
180
+
181
+ <ConsentProvider mode={manifest()} {scripts}>
182
+ <!-- Keep your application, consent UI and preferences link here. -->
183
+ </ConsentProvider>
184
+ ```
185
+
186
+ Retain the styles and consent UI from the [Svelte quickstart](https://c15t.com/docs/frameworks/svelte/quickstart).
187
+ The provider owns the loader and disposes it on unmount.
188
+
189
+ **SvelteKit**
190
+
191
+ Add the scripts to the existing `ConsentRoot` in the root layout. Keep the
192
+ handle and the layout load from the [SvelteKit quickstart](https://c15t.com/docs/frameworks/sveltekit/quickstart).
193
+
194
+ ```svelte title="src/routes/+layout.svelte"
195
+ <script lang="ts">
196
+ import { ConsentRoot } from 'c15t/svelte';
197
+ import { scripts } from '../consent-scripts';
198
+
199
+ let { children, data } = $props();
200
+ </script>
201
+
202
+ <ConsentRoot state={data.consent} {scripts}>
203
+ {@render children()}
204
+ <!-- Keep your consent UI and preferences link here. -->
205
+ </ConsentRoot>
206
+ ```
207
+
208
+ Import vendor helpers in the layout component, not in `+layout.server.ts`:
209
+ a server load cannot send functions to the browser. Prerendered, static and
210
+ SPA-mode pages use the same `scripts` prop. If you pass an externally owned
211
+ `runtime` to the provider, register scripts when creating that runtime instead.
212
+
213
+ **HTML**
214
+
215
+ The helpers in `@c15t/integrations` are ES modules that need a bundler. On a
216
+ page that loads the c15t script tag, paste the vendor's own snippet instead
217
+ and keep it inert until its category is allowed:
218
+
219
+ ```html
220
+ <script type="text/plain" data-c15t-category="measurement">
221
+ // The vendor's snippet, unchanged
222
+ </script>
223
+ ```
224
+
225
+ Use the category this guide names for the vendor. c15t runs the snippet
226
+ once that category is allowed, and reloads the page when the visitor
227
+ withdraws it. Helper options on this page, such as `loadMode`, do not apply
228
+ to a pasted snippet. See [HTML scripts](https://c15t.com/docs/frameworks/html/scripts).
229
+
230
+ **JavaScript**
231
+
232
+ Pass the scripts to `init()` from `@c15t/browser`, next to your mode:
233
+
234
+ ```ts
235
+ import { init, manifest } from '@c15t/browser';
236
+ import { scripts } from './consent-scripts';
237
+
238
+ const consent = init({
239
+ mode: manifest(),
240
+ scripts,
241
+ });
242
+ ```
243
+
244
+ Keep the mode from your quickstart. With
245
+ `createConsentRuntime` from `c15t/runtime`, pass `scripts` to it instead.
246
+ A kernel you create yourself needs a loader from
247
+ `c15t/modules/script-loader`. Attach one loader per kernel. See
248
+ [JavaScript scripts](https://c15t.com/docs/frameworks/javascript/scripts).
249
+
250
+ Keep the returned script configuration stable across rerenders. The helper
251
+ tracks each loader registration and shares one controller for compatible
252
+ HubSpot configurations on the page.
253
+
254
+ ## Options
255
+
256
+ | Option | Default | Behavior |
257
+ | ----------- | --------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
258
+ | `portalId` | Required | Numeric string or number. Surrounding string whitespace is trimmed. Missing, blank or non-numeric values log `hubspot: missing or invalid portalId` and load nothing. Portal identity ignores leading zeros, but loader URLs must still match. |
259
+ | `region` | None | Lowercase code from the tracking snippet, such as `na1` or `eu1`. Used only when `scriptUrl` is absent or blank. Invalid values on that default path log an error and use `js.hs-scripts.com`. Copy the actual account code; matching the format does not verify that a region exists. |
260
+ | `scriptUrl` | `https://js.hs-scripts.com/<portalId>.js`, or its regional host | Custom loader URL. A nonblank override is trimmed and otherwise used unchanged. Whitespace-only values fall back to the default. This does not proxy child scripts or other HubSpot requests. |
261
+ | `category` | `marketing` | An `IntegrationConsentCondition` for the entire loader. A condition such as `{ and: ['marketing', 'measurement'] }` requires both categories. Replacing it with a weaker condition permits the whole loader and its child requests under that condition. |
262
+ | `chat` | `false` | Set `true` to opt into automatic chat. Any active accepted registration can opt in, but every registration's consent must permit the widget. Omitted or `false` leaves chat manual when no registration opts in. |
263
+ | `nonce` | None | CSP nonce for the inserted loader. Pass it here when required; the provider nonce is not forwarded to this callback-only controller. Surrounding whitespace is trimmed and blank values count as omitted. Later explicit nonces must match the reserved nonce. |
264
+
265
+ The helper inserts an HTTPS loader with `async`, `defer` and the literal ID
266
+ `hs-script-loader`. Register one HubSpot portal per page.
267
+
268
+ The first registered configuration reserves the portal, effective loader URL
269
+ and nonce, even if consent initially denies loading. Portal IDs compare without
270
+ leading zeros. Loader URLs compare after the browser resolves them against the
271
+ page URL. Region matters only through the selected URL; a nonblank `scriptUrl`
272
+ override makes `region` irrelevant, including its validation.
273
+
274
+ Later configurations must match the portal and effective URL. An omitted nonce
275
+ accepts the reserved nonce; an explicit nonce must match it exactly. A conflict
276
+ logs a warning and that configuration cannot control the installed SDK or send
277
+ it grants. Its consent also does not restrict accepted owners. The helper never
278
+ overwrites the selected loader or switches the global SDK to another account.
279
+ The reserved identity survives final teardown. Reload the page to change it.
280
+
281
+ An externally installed `hs-script-loader` with a matching URL can be reused,
282
+ with a warning. Its nonce must also match if the reserved configuration chose
283
+ an explicit nonce. A different URL or incompatible explicit nonce blocks
284
+ controller startup until reload, because c15t cannot safely control that SDK.
285
+ Reusing an external script cannot gate requests it already sent.
286
+
287
+ If a non-script element has that ID, the helper leaves it untouched and does
288
+ not insert a loader. It warns once for that element after consent permits
289
+ loading. Remove or rename the conflicting ID, then reload the page to retry
290
+ loader insertion. Removing the element alone does not trigger a retry.
291
+
292
+ Core load and error callbacks describe controller setup, not the external
293
+ download. Check the actual HubSpot request in Network to diagnose download
294
+ failures.
295
+
296
+ The helper starts in the browser after hydration, including when the visitor
297
+ has a saved grant. It does not use an inline server-start snippet.
298
+
299
+ ## Consent categories
300
+
301
+ | c15t category | HubSpot consent field |
302
+ | --------------- | --------------------- |
303
+ | `measurement` | `analytics` |
304
+ | `marketing` | `advertisement` |
305
+ | `functionality` | `functionality` |
306
+
307
+ Each active, accepted loader registration is an owner. Registering the same
308
+ returned script with separate loaders creates separate owners. Distinct helpers
309
+ with compatible installation options share the same controller too.
310
+
311
+ For each owner, the helper evaluates that owner's complete loader condition and
312
+ vendor permission against its own consent state. A failed gate denies all three
313
+ HubSpot fields for that owner. Otherwise, the fields follow the table, with
314
+ missing categories treated as `false`.
315
+
316
+ The controller then intersects those per-owner fields. A shared HubSpot field
317
+ is `true` only when every owner permits it. Compound conditions are evaluated
318
+ per owner before combining the results. A later granted callback cannot
319
+ override another active owner's denial. Initially denied owners restrict the
320
+ controller too, even if their registrations have never loaded. Startup requires
321
+ at least one owner and every owner's loader gate to pass.
322
+
323
+ c15t's `necessary` and `experience` categories have no corresponding field in
324
+ this helper. HubSpot manages its necessary cookies separately.
325
+
326
+ The helper forwards these values through `_hsp.push(['setHubSpotConsent', ...])`
327
+ before the first loader insertion and on later changes. HubSpot does not save
328
+ these values in its consent cookies, so the helper sends them again on each
329
+ consented page load. Before aggregate permission allows startup, it creates no
330
+ HubSpot globals or queues. Existing queues and conversation settings are
331
+ preserved at startup. A denied owner that joins later restricts the running
332
+ installation, but cannot undo requests already sent.
333
+
334
+ The HubSpot loader can inject analytics, ads pixels, forms, surveys, chat and
335
+ other modules according to your portal settings. Its internal consent fields
336
+ do not provide separate request gates for those modules. A category override
337
+ that permits the loader under functionality alone also permits its other
338
+ network requests. HubSpot documents anonymous aggregate analytics after
339
+ analytics refusal.
340
+
341
+ ## Chat
342
+
343
+ The helper always sets `hsConversationsSettings.loadImmediately = false`
344
+ before inserting the loader. Automatic chat starts when at least one active,
345
+ accepted owner sets `chat: true`. An omitted or `false` option does not veto
346
+ another owner's opt-in. Every owner's loader condition, vendor permission and
347
+ functionality consent must still allow the widget before the controller calls
348
+ `HubSpotConversations.widget.load()`. If the conversations SDK becomes ready
349
+ later, the controller checks the latest shared consent before loading.
350
+
351
+ With no owner opted in, the helper does not register chat listeners or call
352
+ widget load, removal or chat-cookie cleanup merely because `chat` is omitted or
353
+ `false`. Your application owns manual chat. When the last opted-in owner leaves
354
+ and manual owners remain, the controller stops automatic chat management and
355
+ detaches its chat listeners. It does not forcibly remove a widget that the
356
+ remaining consent permits. Global cookie revocation can still clear chat
357
+ identity cookies. Your application must enforce subsequent manual widget
358
+ consent.
359
+
360
+ When automatic chat attaches to a Conversations client, the controller checks
361
+ current shared permissions and requests removal and chat-cookie cleanup if
362
+ chat is denied. This includes a matching external client whose `widgetLoaded`
363
+ event already fired, a late SDK, a replacement client and a manual-to-automatic
364
+ handoff. Initially permitted attachment skips cleanup. A fresh controller whose
365
+ loader or vendor gate is denied remains inactive. When `widget.status().loaded`
366
+ is true, permitted attachment reuses the loaded widget without requesting
367
+ another load. After a subsequent withdrawal removes it, regrant can load it
368
+ again without waiting for a second completion of the original load.
369
+
370
+ While automatic chat is active, loss of shared functionality consent or a
371
+ loader or vendor gate requests widget removal and calls
372
+ `HubSpotConversations.clear({ resetWidget: false })` to clear chat cookies.
373
+ The shared `widgetLoaded` listener also removes a late completion when the
374
+ current permissions deny it. Ordinary nonfinal owner departure keeps this
375
+ guard attached while automatic chat remains active.
376
+
377
+ Deferred removal and chat-cookie cleanup check the current shared permissions
378
+ and chat opt-in when the SDK becomes ready. If permission returns before
379
+ readiness, the controller skips that cleanup, including after a remount or
380
+ replacement. A withdrawal that still denies automatic chat clears it when the
381
+ SDK becomes ready. Earlier global cookie revocation can still have cleared
382
+ chat identity cookies.
383
+
384
+ Widget removal does not confirm cancellation of a pending load. The controller
385
+ retains that pending-load state through removal, final teardown and remounts
386
+ to avoid issuing a duplicate load. Final teardown detaches the controller's
387
+ exact ready callback and widget listener. The late-completion guard applies
388
+ while automatic chat management is active; it does not guarantee that an
389
+ in-flight widget can never appear after final teardown. Cleanup does not
390
+ recreate the widget, and removal and cookie cleanup do not guarantee that
391
+ remote chat requests stop.
392
+
393
+ Chat visitor identification needs a server-generated token and is outside
394
+ this helper's configuration. Follow the
395
+ [HubSpot conversations SDK documentation](https://developers.hubspot.com/docs/api-reference/latest/conversations/chat-configuration/chat-widget-sdk)
396
+ for that separate setup.
397
+
398
+ ## Withdrawal and page reload
399
+
400
+ When any previously granted shared HubSpot field becomes denied, the controller
401
+ synchronously queues `revokeCookieConsent`, then sends the remaining shared
402
+ grants. This includes marketing-only withdrawal without a measurement grant
403
+ and loss of functionality. Denying any owner's loader condition or vendor
404
+ denies all three shared fields and requests revocation if any was previously
405
+ granted.
406
+
407
+ HubSpot's `setHubSpotConsent` records and distributes consent state. It does
408
+ not replace cookie revocation. `revokeCookieConsent` takes no category argument
409
+ and resets HubSpot's categories too, so the helper sends the remaining grants
410
+ after it. A partial withdrawal can therefore clear tracking cookies associated
411
+ with categories that remain granted. HubSpot may create cookies again under
412
+ those remaining grants.
413
+
414
+ Global revocation can remove chat identity cookies even when functionality
415
+ remains granted. Restoring functionality consent does not preserve that chat
416
+ identity.
417
+
418
+ Removing a granted owner while other accepted owners remain releases only
419
+ that registration's restriction. If shared permission stays the
420
+ same, departure sends no revocation or all-denied pulse and causes no widget
421
+ removal or extra load. Before releasing an owner, the controller reads its
422
+ current core permission so a real category or vendor withdrawal still takes
423
+ effect, including a withdrawal saved during a synchronous callback.
424
+
425
+ When the last owner leaves, teardown detaches chat callbacks and listeners.
426
+ It forwards any actual consent withdrawal before releasing that owner, but
427
+ does not turn a saved grant into denial or revoke cookies solely because the
428
+ provider unmounted. Replacing a granted configuration preserves the current
429
+ permission across the zero-owner boundary. A remount forwards current consent
430
+ to the retained runtime and reattaches chat management when opted in. If consent
431
+ became denied while no owner was mounted, the remount sends that denial and
432
+ cleans up automatic chat without downloading HubSpot again. Keep the returned
433
+ script configuration stable.
434
+
435
+ Save a withdrawal before disposing the loader. Once the last HubSpot
436
+ registration has been released, the helper no longer observes that owner's
437
+ consent. A denial saved by a later sibling's `onDispose` callback is not
438
+ forwarded, even during the same synchronous teardown. Without a remount or
439
+ page reload, the retained SDK keeps its last forwarded consent. The remount
440
+ behavior above also applies to this case.
441
+
442
+ `revokeCookieConsent` asks HubSpot to remove tracking and consent cookies. It
443
+ can itself trigger a revocation request. It does not remove every browser
444
+ storage entry or unload the child scripts.
445
+
446
+ The helper retains its loader through consent withdrawal and provider
447
+ remounts to avoid running HubSpot's bootstrap again. Keep c15t's default
448
+ `reloadOnConsentRevoked` enabled. Reload ends the running page, and the saved
449
+ denial prevents a fresh load. With reload disabled, previously loaded HubSpot
450
+ code and child requests can continue despite the denied consent signal.
451
+ Reload also cannot undo data already sent.
452
+
453
+ ## Identification and route changes
454
+
455
+ HubSpot's analytics script sends its initial page view. The helper does not
456
+ add another page view or identify the visitor. If your app identifies a known
457
+ contact, call HubSpot's `identify` API only after the configured condition and
458
+ vendor consent allow it. Never use a shared placeholder email for anonymous
459
+ visitors.
460
+
461
+ For SPA navigation, HubSpot uses `setPath` followed by `trackPageView`. Gate
462
+ those application calls on the current consent and vendor permission too.
463
+ See the [tracking-code API](https://developers.hubspot.com/docs/api/events/tracking-code).
464
+
465
+ Review your portal's non-HubSpot forms collection, ads pixels and tracking-data
466
+ use for Breeze Intelligence. These settings change the data and modules that
467
+ HubSpot loads. [Clearbit](./clearbit.md) is a separate tag with a
468
+ separate helper and publishable key.
469
+
470
+ ## Verify HubSpot
471
+
472
+ Start with a fresh browser session and open DevTools Network. Before deciding,
473
+ then with only measurement or functionality allowed, check that no HubSpot
474
+ loader or child requests appear. Allow marketing and check for exactly one
475
+ `hs-script-loader`, the expected portal URL and no HubSpot banner.
476
+
477
+ Check HubSpot's consent state with measurement and functionality toggled
478
+ independently. With `chat: true`, verify that chat appears only after
479
+ functionality is allowed. Test functionality withdrawal while the SDK and
480
+ widget are still loading, then test regrant and provider remounts. If adopting
481
+ an existing SDK with automatic chat initially denied, check that removal and
482
+ cleanup are requested without waiting for another `widgetLoaded` event.
483
+
484
+ If you share the configuration across loaders or use compatible helpers,
485
+ check that an initially denied owner restricts all shared fields. Remove one
486
+ granted owner and verify that a permitted survivor keeps its widget and late
487
+ completion guard without another load or revocation. With no chat opt-in,
488
+ verify that manual widget loading does not trigger helper removal or cleanup.
489
+ Also test the last opt-in leaving while manual owners remain.
490
+
491
+ Test marketing withdrawal with measurement never granted. The privacy queue
492
+ must receive `revokeCookieConsent` before the new consent state. With a custom
493
+ loader condition, also test partial withdrawal and check that remaining grants
494
+ reach HubSpot after revocation. Inspect actual cookies and requests for your
495
+ portal, since queued commands alone do not prove cookie removal or shutdown.
496
+
497
+ The portal can contact `*.hs-scripts.com`, `*.hs-analytics.net`,
498
+ `*.hs-banner.com`, `*.hscollectedforms.net`, `*.usemessages.com`,
499
+ `*.hsadspixel.net`, `*.hubspot.com`, `*.hubspotfeedback.com` and
500
+ `*.hubapi.com`. Consult
501
+ [HubSpot's CSP guidance](https://knowledge.hubspot.com/domains-and-urls/ssl-and-domain-security-in-hubspot)
502
+ for the products your account uses. The loader URL alone is not a complete
503
+ CSP allowlist.
504
+
505
+ Test in a private window with an opt-in policy. Open DevTools Network, disable
506
+ the cache and filter by the vendor's domain:
507
+
508
+ 1. Load the page. No request goes to the vendor before you choose.
509
+ 2. Click Reject, then reload. There is still no vendor request.
510
+ 3. Open Privacy settings and allow the helper's category. The vendor script
511
+ loads without a page reload.
512
+ 4. Turn the category off again and save. c15t reloads the page, and the new
513
+ page makes no vendor request.
514
+
515
+ c15t reloads on revocation because removing a script element does not stop
516
+ code that already ran. The vendor's listeners, timers and queued events stay
517
+ alive until the page unloads. If you set `reloadOnConsentRevoked: false`, stop
518
+ the vendor yourself. Register a callback-only script whose `onConsentChange`
519
+ calls the vendor's opt-out API, as shown in
520
+ [custom integrations](./building-integrations.md), and check the
521
+ permission before each of your own event calls. The reload does not delete
522
+ cookies the vendor already set; see
523
+ [clear on revocation for your framework](./overview.md#vendor-switches-and-cookie-cleanup).
524
+
525
+ The helper lists its vendor by name in the preference dialog, so a visitor can
526
+ turn it off inside an allowed category. See
527
+ [vendor consent for your framework](./overview.md#vendor-switches-and-cookie-cleanup). The
528
+ [consent verification guide](../guides/verify-consent.md) covers navigation,
529
+ expiry and hosting checks.