A **consent bridge** loads a supported vendor and forwards the visitor's decision
to it, live — so the tag keeps sending privacy-safe signals under denied consent
instead of being hard-blocked into silence. It's the "bridge" half of
[bridge, then block](/how-it-works/#bridge-then-block), and it's the setup most
sites get wrong when they wire each vendor by hand.

Every bridge is **one config flag**. The CMP ships and manages the
vendor-specific adapter; you don't write `onConsentChange` glue or custom tag
templates:

```js
lightning("init", {
  bridges: {
    google: { tagId: "G-XXXXXXX" }, // CMP loads gtag.js for you
    hubspot: { portalId: "1234567" }, // CMP loads HubSpot + forwards consent
  },
});
```

Bridges are **opt-in** — omit a key and that vendor is never loaded, and the page
pays no runtime cost for it. (Google Consent Mode *signals* are on by default via
[`googleConsentMode`](/configure/consent-mode/); the `google` bridge below adds
CMP-managed tag *loading* on top.)

> **Tip — When to bridge vs. block**
> A bridge is only possible for vendors with a consent-aware mode. A vendor with
> no such mode (Meta Pixel, TikTok, LinkedIn, Hotjar…) isn't a bridge candidate
> — gate it with the [script blocker](/integrations/script-blocker/) instead.

## Google (gtag / GTM)

Set `bridges.google.tagId` to a GA4 measurement ID (`G-…`), a Google Ads
conversion ID (`AW-…`), or a **GTM container** (`GTM-…`) and the CMP loads it
itself:

```js
lightning("init", { bridges: { google: { tagId: "G-XXXXXXX" } } });
```

The win is **ordering**. The CMP pushes `consent default` (denied) *before* the
tag executes — the single most common Consent Mode mistake when hand-rolling the
head snippet is getting that order wrong and leaking before the default lands.
Letting the CMP own the tag makes the ordering impossible to get wrong.

- **GTM containers work too.** A `GTM-…` id loads your container the canonical
  way (`gtm.start` / `gtm.js`) with the same guaranteed
  consent-default-before-boot ordering. You still gate the container's
  **individual tags inside GTM** via each tag's Consent Settings — the bridge
  only handles *loading* plus Consent Mode signals — so confirm they fire in
  **GTM Preview** after wiring it up.
- If you already hand-place `gtag.js` or the GTM snippet, don't set `tagId`;
  Consent Mode signals still flow via `googleConsentMode`.
- The loader is injected **off the critical path** (idle-time, `async`), so it
  never blocks first paint.

> **Note — The \<noscript> iframe is not included**
> A script-injected bridge can't add GTM's `<noscript>` fallback — and it's moot
> for CMP-loaded tags, since a no-JS visitor never reaches the bridge. If you need
> tags to fire for JS-disabled visitors, paste GTM's `<noscript>` iframe
> **statically** into `<body>` yourself.

## HubSpot

Set `bridges.hubspot.portalId` (the number in `js.hs-scripts.com/<id>.js`):

```js
lightning("init", { bridges: { hubspot: { portalId: "1234567" } } });
```

HubSpot is a **gated** loader, because its tracker fans out to whatever the
portal has connected — including **HubSpot Ads** pixels (which chain to LinkedIn
Insight and similar), and those have **no** cookieless mode.

- **Before consent →** the tracker does **not** load. Nothing HubSpot-related
  touches the page — no cookies, no requests.
- **On grant →** the tracker loads and consent is forwarded: with **statistics
  granted** it does full, cookie-based tracking; with **statistics denied** (but
  the gate open) it records **anonymous, cookieless** page views.

By default the gate is the **`marketing`** category — the one that governs the ad
fan-out. Override it with `loadOn` only for a portal with **no** ad pixels
connected:

```js
// Analytics-only portal: load as soon as statistics is granted.
lightning("init", {
  bridges: { hubspot: { portalId: "1234567", loadOn: "statistics" } },
});
```

`loadOn` accepts `"marketing"` (default), `"statistics"`, or `"either"`.

## Adobe (Experience Platform Web SDK)

Set `bridges.adobe.scriptUrl` to your Web SDK loader — the Adobe Tags (Launch)
embed (`assets.adobedtm.com/…`) or a self-hosted `alloy.js`:

```js
lightning("init", {
  bridges: {
    adobe: { scriptUrl: "https://assets.adobedtm.com/…/launch-….min.js" },
  },
});
```

- **Statistics denied →** the Web SDK's `collect` purpose is set to **no**, so
  queued events are discarded rather than sent. Adobe collection has no
  cookieless middle tier, so denial is a clean opt-out.
- **Statistics granted →** `collect` is set to **yes** and events flow.
- The `setConsent` command (Adobe consent standard `2.0`) is queued **before**
  the loader runs, so nothing is sent under an unknown state.
- Set `instanceName` if your Web SDK uses a command name other than `alloy`.

## Pardot / Marketing Cloud Account Engagement

Set `bridges.pardot.accountId` (`piAId`) and `bridges.pardot.campaignId`
(`piCId`):

```js
lightning("init", {
  bridges: { pardot: { accountId: "1234", campaignId: "5678" } },
});
```

- **Marketing denied →** the CMP sets `pi_opt_in=false` before `pd.js` loads, so
  Account Engagement records nothing. It's a two-state tracker (no cookieless
  mode), so denial is a clean opt-out.
- **Marketing granted →** `pi_opt_in=true` and prospect tracking runs. Because the
  tag is *loaded* (not blocked), a later grant upgrades tracking **live, without
  a page reload**.
- Set `trackerDomain` if you serve `pd.js` from your own first-party tracking
  domain (default `pi.pardot.com`).

## At a glance

| Bridge  | Config key(s)                     | Gated on                  | Under denial                                       |
| ------- | --------------------------------- | ------------------------- | -------------------------------------------------- |
| Google  | `google.tagId`                    | per-signal (Consent Mode) | cookieless pings, modelling recovers data          |
| HubSpot | `hubspot.portalId`                | `marketing` (default)     | not loaded until gate opens; then cookieless views |
| Adobe   | `adobe.scriptUrl`                 | `statistics`              | events discarded (clean opt-out)                   |
| Pardot  | `pardot.accountId` + `campaignId` | `marketing`               | `pi_opt_in=false` (clean opt-out)                  |

## Need a vendor that isn't here?

More bridges are added over time. For anything the CMP doesn't ship an adapter
for, host a small bridge file yourself — the CMP drives it exactly like a
built-in one. See [Custom bridges](/integrations/custom-bridges/).
