> ## Documentation Index
> Fetch the complete documentation index at: https://docs.runconverge.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Implementing Google Tag Gateway

> Serve Google tags from your own domain with Cloudflare and Converge

export const OutboundLink = ({linkText, linkTarget}) => {
  return <a target="_blank" href={linkTarget}> {linkText} 
    <div className="inline h-3 w-3 fill-gray-500 dark:fill-gray-100 text-gray-500 dark:text-gray-300 ml-1 mr-1">
      <svg className="inline w-2.5 h-2.5 bg-gray-500 dark:bg-gray-300" style={{
    maskImage: `url('https://mintlify.b-cdn.net/v6.5.1/solid/up-right-from-square.svg')`,
    maskRepeat: "no-repeat",
    maskPosition: "center center"
  }}></svg> 
    </div>
    </a>;
};

**Google Tag Gateway** serves the Google Tag from your website's domain. The browser loads the tag from a path on your site. Some measurement requests go to that same path. Cloudflare forwards them to Google.

Without the gateway, the page requests `www.googletagmanager.com` and sends hits straight to Google. With it, those requests go to a first-party path you reserve, such as `https://www.example.com/98a7hfca`.

First-party serving is more durable against browser restrictions and some blockers, so Google gets a more complete signal. See the <OutboundLink linkText="Google Tag Gateway documentation" linkTarget="https://developers.google.com/tag-platform/tag-manager/gateway" />.

```mermaid actions={false} theme={null}
flowchart LR
  convergePixel[Converge pixel] -->|"loads gtag from /98a7hfca"| browser[Browser]
  browser -->|"GET /98a7hfca"| cloudflare[Cloudflare]
  cloudflare -->|"fps.goog"| gateway[Google Tag Gateway]
  gateway --> googleProducts[Google Ads]
```

Google Tag Gateway is separate from [Set up a proxy](/sources/proxy). The Converge proxy serves Converge from your domain. This guide serves Google from your domain.

<Warning>
  Don't use Cloudflare's one-click Google Tag Gateway, or Google's in-UI Cloudflare connect. Those setups inject and rewrite tags on their own. Converge needs to load the Google Tag, so set up a self-service Worker or Snippet and then tell Converge the path.
</Warning>

***

## Before you start

* Use Cloudflare as the DNS service for your domain.
* Create one Worker or Snippet and one path per Google Tag ID. `G-`, `AW-`, and `GT-` IDs are separate tags unless they share the same tag ID.
* Pick your own unused path. Don't use a readable word like `/gtg`, `/gtm`, or `/metrics`. Use a random string, for example `/98a7hfca`. Don't copy the example path onto a live site. Don't use `/`.
* Set up the Converge destinations that load that tag: [GA4](/destinations/integrations/ga4), [Google Ads conversions (browser)](/destinations/integrations/google-ads-conversions), [Google Ads conversions (server)](/destinations/integrations/google-ads-conversions-server-side), and [Google Ads remarketing](/destinations/integrations/google-ads-remarketing).

***

## Choose Worker or Snippet

Converge doesn't care which one you use. Both send your measurement path to Google. Pick a tab in [Set up Cloudflare](#set-up-cloudflare) and follow that path.

|           | **Worker**                                                               | **Snippet**                                       |
| :-------- | :----------------------------------------------------------------------- | :------------------------------------------------ |
| Use when  | Your zone is on Cloudflare Free, or you want logs                        | Your zone is already Pro, Business, or Enterprise |
| Plan      | Workers Paid for production. Free is capped at 100,000 requests per day. | Included on Pro and above. No request cap.        |
| Debugging | Worker logs and metrics                                                  | No Worker logs                                    |

***

## Set up Cloudflare

Use the tabs to switch between **Worker** and **Snippet**. After this section, every step is the same.

<Tabs>
  <Tab title="Worker">
    Add a Worker for each Google Tag. Then attach it to a path-scoped route on the proxied hostname.

    <Warning>
      The Cloudflare Workers **Free** plan caps Worker requests at **100,000 per day**, shared across every Worker on the account. The cap resets at midnight UTC. Google Tag Gateway sends the tag script and every measurement hit through the Worker, so a live store can hit this limit quickly.

      Use **Workers Paid** for production. Paid has no daily request cap. See Cloudflare's <OutboundLink linkText="Workers limits" linkTarget="https://developers.cloudflare.com/workers/platform/limits" />.
    </Warning>

    <Steps>
      <Step title="Create the Worker">
        1. In the Cloudflare account home, go to **Compute > Workers & Pages**.
        2. Click **Create application**, then **Start with Hello World!**.
        3. Give it a name that includes the tag ID, for example `google-tag-gateway-g-xxxxxxxx`.
        4. Click **Deploy**.
      </Step>

      <Step title="Paste the Worker code">
        1. Click **Edit code**.
        2. Replace the placeholder with the example below.
        3. Set `TAG_ID` to your Google Tag ID. Use a `G-`, `AW-`, or `GT-` ID.
        4. Click **Deploy**.

        ```javascript theme={null}
        const TAG_ID = "G-XXXXXXXXXX"; // G-, AW-, or GT- ID

        export default {
          async fetch(request) {
            const url = new URL(request.url);
            url.hostname = `${TAG_ID}.fps.goog`;
            url.protocol = "https:";

            const headers = new Headers(request.headers);
            headers.set("X-Gtg-Tag-Id", TAG_ID);
            headers.delete("X-Forwarded-CountryRegion");
            headers.delete("X-Forwarded-Country");
            headers.delete("X-Forwarded-Region");
            headers.delete("X-Forwarded-Geolocation");

            const clientIp = request.headers.get("CF-Connecting-IP");
            if (clientIp) {
              headers.append("X-Forwarded-For", clientIp);
            }

            const cf = request.cf || {};
            const country = cf.country;
            const region = cf.regionCode;

            if (country && country !== "T1" && country !== "XX") {
              if (region) {
                headers.set("X-Forwarded-CountryRegion", `${country}-${region}`);
              } else {
                headers.set("X-Forwarded-Country", country);
              }
            }

            if (cf.latitude && cf.longitude) {
              headers.set(
                "X-Forwarded-Geolocation",
                `latlong=${cf.latitude},${cf.longitude};city=${cf.city || ""}`
              );
            }

            return fetch(url, {
              method: request.method,
              headers,
              body: request.body,
              redirect: "manual",
            });
          },
        };
        ```

        The Worker proxies every request it receives to `{TAG_ID}.fps.goog`. The route, not the Worker, limits which paths reach it. The Worker forwards cookies and query strings, and attaches geolocation from Cloudflare.
      </Step>

      <Step title="Attach a path-scoped route">
        1. Open the Worker and go to **Domains**.
        2. Click **Add route**.
        3. Select the zone you want to use.
        4. Add the route `*.example.com/98a7hfca*`. Replace the hostname and path with the random path you reserved.
        5. Leave **Failure mode** set to **Fail closed (block)** and click **Add Route**.

        Scope the route to the measurement path. The Worker proxies every request it receives, so a catch-all `*` route would send your whole site to Google.
      </Step>

      <Step title="Confirm the hostname is proxied">
        1. Go to the **DNS** settings for your domain.
        2. Find the record for the hostname you used in the route.
        3. Confirm the orange cloud is on (**Proxied**).

        A gray-cloud record never reaches the Worker.
      </Step>
    </Steps>
  </Tab>

  <Tab title="Snippet">
    Add a Snippet for each Google Tag. Then attach a path filter so only the measurement path reaches Google.

    <Warning>
      Snippets need a Cloudflare **Pro**, **Business**, or **Enterprise** zone. They are not available on the Free zone plan. If your zone is on Free, use the **Worker** tab instead or purchase a paid zone plan.
    </Warning>

    This matches Google's <OutboundLink linkText="Cloudflare Snippet sample" linkTarget="https://developers.google.com/tag-platform/tag-manager/gateway/setup-guide?setup=manual" />.

    <Steps>
      <Step title="Create the Snippet">
        1. In the Cloudflare dashboard, open your domain and go to **Rules > Snippets**.
        2. Click **Create Snippet**.
        3. Give it a name that includes the tag ID. Use lowercase letters, numbers, and underscores only, for example `google_tag_gateway_g_xxxxxxxx`.
      </Step>

      <Step title="Paste the Snippet code">
        1. Replace the placeholder with the example below.
        2. Set `TAG_ID` to your Google Tag ID. Use a `G-`, `AW-`, or `GT-` ID.

        ```javascript theme={null}
        const TAG_ID = "G-XXXXXXXXXX"; // G-, AW-, or GT- ID

        export default {
          async fetch(request) {
            const url = new URL(request.url);
            url.hostname = "fps.goog";

            const headers = new Headers(request.headers);
            headers.set("X-Gtg-Implementation", "Snippet");
            headers.set("X-Gtg-Tag-Id", TAG_ID);
            headers.append("X-Forwarded-For", request.headers.get("CF-Connecting-IP"));
            headers.set("X-Forwarded-Country", request.cf.country);
            headers.set("X-Forwarded-Region", request.cf.regionCode);
            headers.set(
              "X-Forwarded-Geolocation",
              `latlong=${request.cf.latitude},${request.cf.longitude};city=${request.cf.city}`
            );

            return fetch(url, new Request(request, { headers }));
          },
        };
        ```

        The Snippet rewrites matching requests to `fps.goog` and identifies the tag with `X-Gtg-Tag-Id`.
      </Step>

      <Step title="Attach a path filter">
        1. Open **Snippet rule** and select **Custom filter expression**.
        2. Click **Edit expression** and paste `(starts_with(http.request.uri.path, "/98a7hfca"))`.
        3. Replace `/98a7hfca` with the random path you reserved.
        4. Click **Done**, then **Deploy**.

        Scope the filter to the measurement path. A broader match would send extra site traffic to Google.
      </Step>

      <Step title="Confirm the hostname is proxied">
        1. Go to the **DNS** settings for your domain.
        2. Find the record for the hostname that serves the site.
        3. Confirm the orange cloud is on (**Proxied**).

        A gray-cloud record never reaches the Snippet.
      </Step>
    </Steps>
  </Tab>
</Tabs>

***

## Point Converge at the path

Set the path on every destination that loads that Google Tag. Don't edit the site snippet. Converge loads `gtag` from `https://www.example.com/98a7hfca` instead of `www.googletagmanager.com`. Use the random path you reserved, not this example.

<Steps>
  <Step title="Set the Google Tag Gateway path">
    1. Open the destination and go to **Configuration**.
    2. Open **Advanced configuration**.
    3. Set **Google Tag Gateway path** to the same random path you used in Cloudflare, for example `/98a7hfca`.
    4. Click **Save**.
  </Step>
</Steps>

Repeat this for each destination that uses the tag:

* [Google Analytics 4](/destinations/integrations/ga4)
* [Google Ads conversions (browser)](/destinations/integrations/google-ads-conversions)
* [Google Ads conversions (server)](/destinations/integrations/google-ads-conversions-server-side)
* [Google Ads remarketing](/destinations/integrations/google-ads-remarketing)

The server-side Google Ads destination still needs the path. Conversions go through the Google Ads API, but Enhanced Conversions for Leads uses the Google Tag on the page.

Destinations that share the same tag ID share the same path. Different tag IDs need different Workers or Snippets, and different paths.

<Note>
  For visitors in the European Economic Area (EEA), Google does not send GA4 events through the Google Tag Gateway path. Those events still use Google's regular regional analytics endpoints. This is Google's privacy behavior, not a Converge setting. Google Ads hits still go through the gateway path.
</Note>

***

## Verify the setup

1. Open `https://www.example.com/98a7hfca/healthy`. The page should read `ok`.
2. Open `https://www.example.com/98a7hfca/?validate_geo=healthy`. The page should read `ok`.
3. Load your site, open DevTools > **Network**, and confirm the Google Tag and Google Ads hits go to your path, not `www.googletagmanager.com`. For EEA visitors, GA4 events still go to Google's regular analytics endpoints.
4. Preview the site in Tag Assistant and confirm Google Ads hits use the measurement path.

Replace the hostname and path with the random path you reserved. Don't use `/98a7hfca` unless that's the path you created.

***

## FAQ

<AccordionGroup>
  <Accordion title="The health check doesn't return ok">
    Confirm the DNS record is **Proxied** and `TAG_ID` is the full Google Tag ID.

    If you used a Worker, confirm the route matches the path (including the `*` suffix) and check the Worker logs. If you used a Snippet, confirm the filter expression matches the path.
  </Accordion>

  <Accordion title="I see Error 1027 or the Google Tag stopped loading">
    This applies to Workers on the Free plan. The account hit the 100,000 requests per day cap. Upgrade to Workers Paid, or wait until midnight UTC when the cap resets. Error 1027 with **Fail closed (block)** stops the Google Tag from loading. Snippets don't have this cap.
  </Accordion>

  <Accordion title="Geo validation fails">
    Cloudflare must set `X-Forwarded-CountryRegion` or `X-Forwarded-Country` from its own geolocation data. Don't forward geo headers the browser sent. Redeploy the example and retry `/{path}/?validate_geo=healthy`.
  </Accordion>

  <Accordion title="The path is already used by the site">
    Pick a different unused random path. Update the Worker route or Snippet filter, and the **Google Tag Gateway path** in every matching Converge destination.
  </Accordion>

  <Accordion title="Can I use /gtg or /metrics?">
    Don't. Readable paths are easier to guess and more likely to collide with a real page. Invent a random path such as `/98a7hfca` and use it in Cloudflare and Converge.
  </Accordion>

  <Accordion title="I have more than one Google Tag">
    Create one Worker or Snippet and one path per Google Tag ID. Set each destination to the path that matches its tag.
  </Accordion>

  <Accordion title="Does this cover Shopify checkout or other hostnames?">
    Only proxied hostnames with a Worker route or Snippet filter serve the tag first-party. Add a route or filter for each hostname that loads the Converge pixel, or pick a path on the storefront domain you already proxy.
  </Accordion>

  <Accordion title="Why do EEA GA4 events still go to Google's regular endpoints?">
    For visitors in the European Economic Area, Google does not route GA4 events through the Google Tag Gateway path. Those hits go to Google's regional analytics endpoints instead. This is Google's privacy behavior, not a Converge setting. The Google Tag and Google Ads hits still use your first-party path.
  </Accordion>

  <Accordion title="Does Google Tag Gateway replace Consent Mode?">
    No. The Google Tag still respects Consent Mode. Set Consent Mode before the Converge pixel loads. See [Implementing Consent Mode](/guides/implementing-consent-mode).
  </Accordion>
</AccordionGroup>
