Skip to content
EARNST.
DE
Tracking & Compliance

GTM API Automation: Tracking Infrastructure as Code Instead of Click Adventures

Configuring GTM containers by hand does not scale. Python scripts provision tags, triggers, and variables reproducibly. Here is how Infrastructure as Code works for tracking.

EARNST · · 12 min read

Key Takeaways

  • The GTM API allows full provisioning of tags, triggers, and variables via script
  • Infrastructure as Code for tracking: versioned, reproducible, audit-ready
  • A single Python script replaces 2 to 3 hours of manual GTM configuration per container
  • Rollbacks in seconds instead of error-prone manual reconstruction

A new Shopify store goes live. The tracking setup requires: GA4 config tag, 8 event tags, 12 triggers, 15 DataLayer variables, Consent Mode defaults, SST forwarding rules. In the GTM interface, that means 40 to 50 individual configuration steps, each with dropdown menus, free-text fields, and checkboxes. 2 to 3 hours of work if everything is correct on the first try.

The second store, same process. The third, same again. And when an audit six months later finds that a trigger is misconfigured, nobody can reliably reconstruct when and why the change happened.

The GTM API solves this problem. It allows full provisioning of a GTM container via script: tags, triggers, variables, consent settings, workspace management, versioning. Everything clickable in the GTM interface is automatable via the API.

Why manual GTM setup does not scale

Three problems that grow with every additional container.

No reproducibility. Two containers that should be identical differ in 15 details after three months. A trigger has a different operator, a variable has a different DataLayer key, a tag has a forgotten consent setting. These discrepancies do not arise from malicious intent, but from the nature of manual work: every click is a potential error.

No version control. GTM has built-in versions, but no diffs. You see "Version 47 was published", but not "in Version 47, the GA4 event tag 'purchase' was switched from Trigger A to Trigger B". For an audit, this is useless. You need traceable change history, not just version numbers.

No rollback. If Version 48 contains an error, you can roll back to Version 47. But only the entire container. If you want to roll back just one tag, you have to reconstruct it manually. With complex setups of 30+ tags, this is error-prone and time-consuming.

What the GTM API can do

The Google Tag Manager API (v2) provides full access to all container resources:

Accounts and containers. Create, configure, and list containers. Both web containers and server containers.

Workspaces. Create workspaces where changes are prepared before publication. Comparable to feature branches in Git.

Tags. Create and configure every tag type: GA4 Config, GA4 Event, Google Ads Conversion, Custom HTML, Server-Side Clients. Including consent settings, firing priority, and tag sequences.

Triggers. All trigger types: Custom Events, Page Views, Element Visibility, Timer, History Changes. With any filter conditions.

Variables. DataLayer variables, JavaScript variables, Lookup Tables, RegEx Tables, Constant variables. Everything configurable as a variable in GTM.

Versions. Create, publish, and compare container versions. Including the live version and drafts.

The architecture: Python as provisioning layer

Python is the right automation language for three reasons: Google's official client library (google-api-python-client) is mature and well-documented, Python scripts are readable (even for non-developers on the team), and the scripts integrate into CI/CD pipelines.

Authentication

The GTM API uses OAuth 2.0. For automation, a service account is the right choice:

  1. Google Cloud Console: create a project
  2. Enable the GTM API
  3. Create a service account and download the JSON key
  4. Add the service account as a user with "Edit" permissions in the GTM container

The JSON key is referenced as an environment variable or secret, never committed to the repository.

Container definition as JSON

The core principle: the desired state of the container is defined as a JSON file. The script reads the definition and provisions the container accordingly.

{
  "container": "Shopify Production",
  "tags": [
    {
      "name": "GA4 - Config",
      "type": "gaawc",
      "parameter": [
        { "key": "measurementId", "value": "G-XXXXXXXXXX" },
        { "key": "sendPageView", "value": "true" }
      ],
      "consentSettings": {
        "consentStatus": "needed",
        "consentType": { "ad_storage": true, "analytics_storage": true }
      }
    }
  ],
  "triggers": [
    {
      "name": "CE - consent_given",
      "type": "customEvent",
      "customEventFilter": [
        { "parameter": [{ "key": "arg0", "value": "consent_given" }] }
      ]
    }
  ],
  "variables": [
    {
      "name": "DLV - ecommerce.transaction_id",
      "type": "v",
      "parameter": [
        { "key": "name", "value": "ecommerce.transaction_id" },
        { "key": "dataLayerVersion", "value": "2" }
      ]
    }
  ]
}

Idempotent provisioning

The script checks the current state against the desired state on every run. If a tag already exists with an identical configuration, it is skipped. If it exists with a different configuration, it is updated. If it does not exist, it is created. This principle (idempotency) prevents duplicates and makes repeated execution safe.

def provision_tag(service, workspace_path, tag_config, existing_tags):
    """Creates or updates a tag based on the configuration."""
    match = find_by_name(existing_tags, tag_config["name"])

    if match and config_matches(match, tag_config):
        return {"action": "skipped", "tag": tag_config["name"]}

    if match:
        result = service.accounts().containers().workspaces().tags().update(
            path=match["path"],
            body=build_tag_body(tag_config)
        ).execute()
        return {"action": "updated", "tag": result["name"]}

    result = service.accounts().containers().workspaces().tags().create(
        parent=workspace_path,
        body=build_tag_body(tag_config)
    ).execute()
    return {"action": "created", "tag": result["name"]}

Workspace workflow

Changes are never made directly in the default workspace. The script creates a new workspace (comparable to a feature branch), provisions all changes there, and publishes only after validation.

  1. Create workspace: workspaces().create()
  2. Provision tags/triggers/variables
  3. Check workspace status: workspaces().getStatus()
  4. On conflicts: resolve or abort
  5. Create version: versions().create() from the workspace
  6. Publish version: versions().publish()

In practice: provisioning a complete Shopify tracking setup

A concrete example: setting up the tracking from our Enterprise E-Commerce Tracking article via script.

Step 1: Create container definition

The JSON file contains the complete configuration:

  • 1 GA4 config tag (Measurement ID, SST transport URL)
  • 8 GA4 event tags (page_view, view_item, add_to_cart, begin_checkout, add_payment_info, add_shipping_info, purchase, consent_decision)
  • 1 Google Ads conversion tag
  • 12 custom event triggers
  • 15 DataLayer variables (ecommerce fields, consent state, click IDs)
  • Consent Mode defaults

Step 2: Execute script

python provision_gtm.py \
  --config shopify-tracking.json \
  --container GTM-XXXXXXX \
  --workspace "Setup 2026-03-25"

Output:

Workspace 'Setup 2026-03-25' created.
Tags:      9 created, 0 updated, 0 skipped
Triggers: 12 created, 0 updated, 0 skipped
Variables: 15 created, 0 updated, 0 skipped
Workspace status: No conflicts.

Step 3: Validate and publish

Before publishing, the script optionally validates against a rules file: does every tag have a consent setting? Is every trigger linked to at least one tag? Are there orphaned variables?

python provision_gtm.py \
  --config shopify-tracking.json \
  --container GTM-XXXXXXX \
  --workspace "Setup 2026-03-25" \
  --validate \
  --publish

Version control: Git as audit trail

The container definition lives as JSON in the Git repository. Every change to the tracking setup is a Git commit with message, author, and timestamp. This provides exactly the change history that GTM itself does not offer.

commit a3f7c2d (2026-03-20)
Author: tracking-team
    Consent Mode: added ad_user_data and ad_personalization

commit 8b1e4f9 (2026-03-15)
Author: tracking-team
    Purchase event: Web Pixel as primary trigger, Order Status as fallback

For GDPR audits, this is invaluable. The question "When was this consent setting changed?" can be answered in seconds.

Rollbacks

A faulty deployment? Two options:

Container rollback. GTM allows restoring any published version. The script can automate this:

python provision_gtm.py \
  --container GTM-XXXXXXX \
  --rollback-to-version 47

Selective rollback. Only one tag was faulty? Git diff shows the change, the JSON definition is reverted to the previous state, the script provisions only the difference.

git diff HEAD~1 shopify-tracking.json
git checkout HEAD~1 -- shopify-tracking.json
python provision_gtm.py --config shopify-tracking.json --container GTM-XXXXXXX

Multi-container management

For clients with multiple stores or markets, a single script manages multiple containers with shared base configuration and market-specific overrides.

{
  "base": "base-tracking.json",
  "containers": [
    {
      "id": "GTM-AAAAAAA",
      "name": "DE Store",
      "overrides": {
        "measurementId": "G-DE1234567",
        "adsConversionId": "AW-DE1234567"
      }
    },
    {
      "id": "GTM-BBBBBBB",
      "name": "AT Store",
      "overrides": {
        "measurementId": "G-AT1234567",
        "adsConversionId": "AW-AT1234567"
      }
    }
  ]
}

One command provisions all containers with the identical tag structure but market-specific IDs. Consistency across markets without manual reconciliation.

CI/CD integration

The script integrates into any CI/CD pipeline. An example with GitHub Actions:

name: GTM Deploy
on:
  push:
    paths: ['tracking/*.json']
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: '3.12'
      - run: pip install google-api-python-client google-auth
      - run: |
          python provision_gtm.py \
            --config tracking/shopify-tracking.json \
            --container $ \
            --validate \
            --publish
        env:
          GOOGLE_APPLICATION_CREDENTIALS: $

Result: every change to the container definition in the main branch automatically provisions the GTM container. No more manual GTM logins.

Limits of automation

Not everything can or should be automated.

GTM Preview and debugging remain manual. The API can provision containers, but not test them. For validating tag logic, you still need GTM Preview Mode or a consent debugging tool.

Custom HTML tags with complex logic are difficult to maintain as JSON. When a tag contains 50 lines of JavaScript, the JSON representation becomes unwieldy. Better: manage the JavaScript as a separate file and embed it into the tag configuration via script.

Consent settings require legal understanding. The script can provision consent configurations, but it cannot decide which tags need which consent. That decision stays with humans.

Conclusion

Configuring GTM containers by hand is like managing infrastructure via SSH: it works for the first server, but not for the tenth. The GTM API turns tracking configuration into reproducible, versioned, audit-ready infrastructure.

The effort for the initial script setup is a few hours. After that, every container provisioning saves 2 to 3 hours of manual work and eliminates the most common source of error: human inattention during repetitive configuration.

Want to automate your tracking infrastructure? In our tracking setup, API-based provisioning is the standard, not a premium add-on.

Our service

Tracking & Data Architecture

20–40% of your conversion data is missing. Server-side tracking, Consent Mode v2, 18+ events, and engagement scoring bring it back.

Learn more