# Website Tracking IDs: GA4, GTM, Google Ads, Meta Pixel (`income_wizard/website-tracking-ids`) Actor

Get the real GA4, GTM, Google Ads and Meta Pixel IDs of any website, plus other ad and analytics tags, including live tags inside Google Tag Manager. Optional browser mode.

- **URL**: https://apify.com/income_wizard/website-tracking-ids.md
- **Developed by:** [Illia Boyko](https://apify.com/income_wizard) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.00 / 1,000 website analyzeds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Website Tracking IDs: GA4, GTM, Google Ads, Meta Pixel and more

Give it websites, get back the **actual tracking IDs** each one runs: Google Analytics 4 (`G-…`), Universal Analytics, Google Tag Manager (`GTM-…`), Google Ads (`AW-…`), Floodlight, Meta Pixel, LinkedIn Insight, Microsoft Ads UET, X, Reddit, Snap, Clarity, Hotjar and others. Plain HTTP by default (cheap and fast); an optional browser mode finds the tags that JavaScript adds after the page loads. No proxy, no login.

**What makes it different from a tech-stack detector:** most big sites load GA4, Google Ads and ad pixels through Google Tag Manager, so the page HTML shows only `GTM-XXXX`. This actor opens that public GTM container, reads it as data (not as text), and returns the IDs of the **live** tags inside it. Paused tags are skipped, and constant GTM variables are filled in. On allbirds.com the page HTML alone shows 2 tools (Clarity, GTM); with the container read it shows 8, including 9 GA4 IDs, 2 Google Ads IDs and a TikTok Pixel ID.

### Who uses it

- **Ad and analytics agencies** auditing a client's or a competitor's tracking setup, or checking that a pixel is on the site.
- **Lead-gen and sales teams** qualifying companies by what they run (a Meta Pixel or Google Ads ID means they buy ads).
- **Researchers and OSINT** linking sites that share one analytics or ad account.

### Input

```json
{"urls": ["hubspot.com", "https://www.allbirds.com"], "readGtmContainers": true, "browser": "off"}
```

| Field | Meaning |
|---|---|
| `urls` | Domains or URLs, one per line. |
| `readGtmContainers` | Read the GTM containers found on the page (default on). |
| `browser` | `off` (default): plain HTTP. `fallback`: pages that return no ID, or refuse the plain request (403/429), are opened again in Chromium. `always`: every page is opened in Chromium. |
| `concurrency` | Sites fetched in parallel, default 5. |
| `maxUrls` | Stop after this many sites. |

### Output (one row per website)

```json
{
  "url": "https://hubspot.com/",
  "ok": true,
  "ids": {"ga4": ["G-LXTM6CQ0XK"], "gtm": ["GTM-N5LT88"], "meta_pixel": ["893596480692132"]},
  "technologies": [{"name": "Google Analytics 4", "kind": "analytics", "ids": ["G-LXTM6CQ0XK"], "sources": ["gtm:GTM-N5LT88"]}],
  "gtmContainers": [{"container": "GTM-N5LT88", "fetched": true, "liveTags": 385, "pausedTags": 6}]
}
```

`sources` says where each ID was found: `page`, `gtm:GTM-…`, and in browser mode also `page (rendered)` and `network request`. (The example shows three of the ID types the row holds.)

### Shared IDs across your list

When you give several sites, the run ends with one extra free row (`"type": "sharedIds"`) listing the analytics, ad, tag-manager and CRM IDs that appear on more than one of them, which points to the same owner or agency. Test on 126 real sites: one shared ID, the GA4 ID `G-C3QZPB4GVE` on theverge.com and vox.com (found in both pages' HTML). Most unrelated companies share nothing, so expect an empty result on a random list.

### What the tests showed

Run on 136 test rows (real company sites, 95 fetched, 41 blocked or failed): 62 sites returned at least one ID. Of the sites that had a GTM container, all 38 containers were read. Every one of the 562 IDs returned was checked to exist as text in the page or container it came from (562 of 562). Every tool in the table came back with a non-empty ID each time it was detected, except where noted:

| Tool | Sites detected | With an ID |
|---|---|---|
| Google Tag Manager | 37 | 37 |
| Google Analytics 4 | 34 | 34 |
| Google Ads (live tags) | 33 | 33 |
| Google Ads (accounts linked in container settings) | 24 | 24 |
| Meta (Facebook) Pixel | 23 | 23 |
| LinkedIn Insight Tag | 20 | 20 |
| Universal Analytics | 20 | 20 |
| Microsoft Advertising UET | 25 | 22 |
| TikTok Pixel | 10 | 10 |
| Hotjar | 4 | 4 |

Most sites simply do not run a TikTok pixel or Hotjar, so those are rarer in the results. Consent tools (OneTrust: 7 detected, 4 with an ID) are often detected by script name only. "Linked in container settings" are Google Ads accounts the container is connected to; they are listed separately from live conversion tags.

### Compared with other Store actors

Same 20 sites (asana.com, monday.com, salesforce.com, wired.com and 16 more) run through this actor and three actors that return tracking IDs, on 2026-10-04. IDs returned in total / sites with at least one ID. Prices are the current ones from each actor's Store record, checked against what our runs of them were charged:

| Actor | Price | IDs | Sites |
|---|---|---|---|
| This actor, `browser: off` | $0.002 per website | 229 | 20 |
| This actor, `browser: always` | $0.008 per website rendered | 324 | 20 |
| smart-digital/website-tags-pixels-detector (real browser) | $0.01 per result | 59 | 19 |
| trovevault/ad-pixel-id-extractor | $0.002 per website + $0.001 per start | 66 | 6 |
| memo23/tiktok-kwai-ads-checker | $0.004 per company + $0.005 per start | 53 | 15 |

With `browser: off` the price equals trovevault's; the difference is the output. Meta Pixel IDs: this actor 21 IDs on 11 sites; trovevault 5 on 3; memo23 0. LinkedIn: 24 IDs on 9 sites against 1 on 1 (trovevault). Microsoft Ads UET: 9 sites against 0.
Where the others found more: smart-digital runs a real browser; on gitlab.com, glossier.com, gymshark.com, klaviyo.com, monday.com and ycombinator.com it returned 16 IDs that the plain mode did not. With `browser: always` this actor returned 14 of those 16 (the 2 it did not return are GA4 IDs on klaviyo.com; our server was redirected to klaviyo.com/de/, where the page carries a different GA4 ID, so these 2 depend on the visitor's country). trovevault returned 21 "Google Ads" IDs (`AW-…`) on salesforce.com, klaviyo.com and techcrunch.com that this actor does not return; in the sites' containers all 21 are LinkedIn conversion IDs (tags carrying a LinkedIn partner ID), not Google Ads accounts.
A second sample of 14 other sites gave the same picture for plain mode: Meta Pixel on 5 sites against 3, LinkedIn on 4 against 0, Microsoft Ads UET on 3 against 0 (this actor against trovevault). Both samples are sites where this actor found at least one ID.

### Browser mode

Many sites add their tags with JavaScript, so the HTML the server sends shows few of them. `browser: always` opens each page in Chromium, waits 4 seconds, and reads the rendered page, the requests it made and the GTM containers it loaded. On the 20 comparison sites it returned 324 IDs against 229 in plain mode, and every ID the plain run had was still there. A page that returns IDs in the browser is charged as "Website rendered in a browser" ($0.008); in `fallback` mode only pages the plain request could not read, and pages that loaded but showed no ID, are opened this way.
Second test, 41 big consumer and SaaS sites, `browser: always`: on the 35 sites that plain mode could fetch, 265 IDs in plain mode and 375 in browser mode, none lost; of the 6 sites that refused the plain request, 2 opened in the browser (canva.com, theguardian.com).
Test on 30 sites not used before, `browser: fallback`: 26 fetched, 22 of them with IDs. Of 8 pages with no IDs in plain mode, the browser found IDs on 4 (trello.com 1, getresponse.com 16, brevo.com 7, gorgias.com 15) and nothing on 4. Of 3 pages that refused the plain request (403) in this set, the browser was refused too.

Second test on 40 more sites not used before (mostly US retail, ticketing and pet/outdoor stores): plain mode fetched 23 and returned 120 IDs on 13 of them (120 of 120 found as text in their page or container); `browser: fallback` fetched 25, returned 204 IDs on 21, and was refused again by the 403 sites (sephora, doordash, ticketmaster, stubhub, seatgeek, sportsmans, cabelas, basspro, petco). Large retail sites often block automated requests: those rows are free.
Limits: it took 1 min 21 s for 20 pages with 3 browsers in parallel (2 min 25 s with one); sites that show tags only after a cookie-consent click (shopify.com, notion.so in our test) still return nothing; sites that block Chromium (lego.com, salesforce.com's browser pass) return 403 and the plain result is kept. Memory is set from your input: 512 MB with `browser: off`; with browser mode 4 GB for 2-3 parallel Chromium browsers (measured peak 1.7 GB with 3) or 2 GB with `browserConcurrency: 1` (measured peak 1.2 GB). You can still change it in the run options.

### Price

**$0.002 per website analyzed** (plain HTTP). **$0.008 per website rendered in a browser** (only when `browser` is `fallback` or `always` and the browser run returned IDs). A website counts when its page was fetched (HTTP below 400). Sites that fail, block the request (403/429) or are not valid URLs are free.

### Limits

- With `browser: off` (default) it reads the HTML the server sends, without running JavaScript. A site that injects its tags only after the page loads (single-page apps) may show few or no IDs; use `browser: fallback` or `always` for those.
- Some sites block automated requests: 24 of 82 big consumer sites in two tests (test sets 5 and 6) returned 403 or 429. Those rows say so and are not charged.
- Only the start page of each URL is read. Sites that serve other tags by country show the ones for the server's country (Germany in our tests).
- An ID in a GTM container belongs to a live tag, but the tag may fire only on some pages or after consent; this actor does not evaluate triggers. An ID the container builds from a runtime variable (for example a Meta Pixel ID taken from the data layer) cannot be read.

# Actor input Schema

## `urls` (type: `array`):

Domains or URLs, one per line. The scheme (https://) is optional.

## `readGtmContainers` (type: `boolean`):

Open each GTM container the page loads and list the GA4, Google Ads, Meta Pixel and other tag IDs published inside it. Most sites keep their IDs there, not in the HTML.

## `concurrency` (type: `integer`):

How many sites are fetched at once.

## `maxUrls` (type: `integer`):

Stop after this many websites.

## `browser` (type: `string`):

off: plain HTTP only (default, cheapest). fallback: pages that return no ID, or that refuse the plain request (403/429), are opened again in Chromium. always: every page is opened in Chromium, so tags injected by JavaScript are found. A page opened in the browser that returns IDs is charged as 'Website rendered in a browser'.

## `browserConcurrency` (type: `integer`):

How many Chromium browsers run at once in browser mode. 1 sets the run memory to 2 GB, 2 or 3 to 4 GB.

## Actor input object example

```json
{
  "urls": [
    "hubspot.com",
    "allbirds.com"
  ],
  "readGtmContainers": true,
  "concurrency": 5,
  "maxUrls": 1000,
  "browser": "off",
  "browserConcurrency": 3
}
```

# Actor output Schema

## `results` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "urls": [
        "hubspot.com",
        "allbirds.com"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("income_wizard/website-tracking-ids").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "urls": [
        "hubspot.com",
        "allbirds.com",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("income_wizard/website-tracking-ids").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "urls": [
    "hubspot.com",
    "allbirds.com"
  ]
}' |
apify call income_wizard/website-tracking-ids --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,income_wizard/website-tracking-ids"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/kf3FQ3gWxgUDZZntj/builds/rWtaNMDjCl8fD0Khx/openapi.json
