Google Tag Manager Integration

Modified on Wed, 26 Aug at 1:09 PM

How the Expian B2C booking flow talks to Google Tag Manager: what to configure in Admin, which events fire, and what each payload contains.

Last updated August 2026 · Covers the current (frontend-v2) booking flow; the legacy "Lapland" booking flow now emits the same events following the analytics port.

1. How the integration works

When GTM is enabled, the booking app injects the tenant's Google Tag Manager container into the page and pushes structured events onto the standard dataLayer as the visitor moves through the booking journey. Nothing is sent directly to Google Analytics by Expian — the GTM container owns what happens next. Tags, triggers, and the GA4 configuration all live in the tenant's container, so the tenant's marketing or analytics team keeps full control over how the data is consumed.

The events follow Google's standard GA4 ecommerce schema (view_items, add_to_cart, begin_checkout, purchase, and so on), plus a small set of Expian-specific events for funnel-step tracking and basket metadata. A tenant's GTM container needs custom-event triggers matching the event names in section 6 for the data to reach GA4.

Two switches must both be on for anything to happen:

  • The tenant configuration (managed by Expian) must have the GTM feature enabled for the B2C app.
  • The market must have Google Analytics enabled in Admin, with a container ID. GTM settings live on the market, so a tenant with several markets can run a different container (or none) per market.

2. Setting it up

Step 1 — Ask Expian to enable the feature

GTM support is switched on per tenant in the platform configuration. When requesting it, the tenant can also choose two optional behaviours:

  • Consent tracking — Expian shows its built-in cookie consent banner and wires it into Google Consent Mode (see section 3). Leave this off if the tenant runs their own consent management platform.
  • Hashed user data capture — checkout events additionally carry the customer's email address and phone number as SHA-256 hashes, for Google's enhanced conversions. The raw values are never sent.

Step 2 — Enter the container details on the market

Admin → Settings → Markets → select the market → Analytics tab

Tick Enable Google Analytics, then fill in:

FIELD WHAT IT IS REQUIRED
Google Analytics Tag ID The GTM container ID, e.g. GTM-ABC1234, from the tenant's Tag Manager account. Yes
Google Analytics Auth The gtm_auth token for a specific GTM environment (e.g. a staging environment). Leave blank to load the live container version. No
Google Analytics Preview The matching gtm_preview environment ID. Used together with Auth. No

Auth and Preview come from the tenant's GTM account under Admin → Environments → Get Snippet, and are only needed when a non-live container environment should load — typically so a test site can run a draft container.

Step 3 — Tag the catalogue entities

Each bookable entity carries an Analytics Tag used to build the item_id that appears in ecommerce events (see section 4). Every entity type with analytics support has an Analytics tab on its edit form:

  • Admin → Products — also offers "Override analytics item ID": when ticked, the product's Analytics Override Value replaces the composed item ID entirely.
  • Admin → Building Blocks → Ticket Types
  • Admin → Building Blocks → Locations
  • Admin → Extras → Add-ons
  • Admin → Entitlements (memberships, patronages)

An entity with no tag appears as a - in the composed item ID, so it is worth tagging locations and ticket types before go-live.

Step 4 — Build the receiving container

In Google Tag Manager, the tenant's team creates custom-event triggers for the event names in section 6 and connects them to GA4 event tags. Because the payloads follow the GA4 ecommerce schema, the standard GA4 tag templates map the ecommerce object automatically.

3. Cookie consent

When consent tracking is enabled, the integration uses Google Consent Mode and Expian's built-in consent banner:

  • Before the container loads, consent defaults are pushed: ad_storage, ad_user_data, and ad_personalization are always denied; analytics_storage is granted only if the visitor previously accepted.
  • The visitor's choice is stored in a cookie named expianGAConsent for 12 months.
  • Accepting or declining the banner pushes a consent update (analytics_storage: granted or denied) — the GTM container and GA4 tags respect this automatically when Consent Mode is configured in the container.

With consent tracking disabled, the container loads unconditionally and consent is assumed to be handled elsewhere (for example by the tenant's own CMP inside GTM).

4. The SKU — how item IDs are built

Throughout the booking flow, whenever an event references a basket item, the platform generates a computed string — the SKU — and sends it as the item's item_id. The SKU is assembled live, per item and per visitor, from the Analytics Tags entered in Admin (step 3 of the setup) plus two visitor-specific flags:

SEGMENT EXAMPLE WHERE IT COMES FROM
Location S The location's Analytics Tag.
Ticket type A The ticket type's Analytics Tag.
Member status W M = the visitor has an active membership; W = no membership (web visitor).
Donation D D if a donation was added, otherwise omitted.
Extension suffix (optional) 24 Product extension fields configured per tenant — see below.

Worked example. A visitor with no membership adds an Adult ticket for Some Location and opts in to a donation. Some Location's Analytics Tag is S and the Adult ticket type's tag is A, so the SKU comes out as:

  S + A + W + D  =  SAWD  

The same booking made by a member without a donation would produce SAM. Because member and donation status are baked into the ID, the one physical ticket type deliberately appears as several SKUs in analytics — that split is the point: it lets reports separate member from non-member sales and donation uptake without extra dimensions.

Extension suffixes

Tenants can append product data to the end of every SKU. Expian configures which product extension fields feed the suffix (per tenant); the values of those fields on each product are then concatenated onto the SKU. The common use is a year marker — a product carrying a year suffix of 24 yields SAWD24 — so annual-pass style products can be compared season against season.

Overrides and special cases

  • Product override — ticking "Override analytics item ID" on a product's Analytics tab replaces the entire composed SKU with the product's Analytics Override Value. Useful when a tenant already has established SKUs in their analytics they need to keep.
  • Add-ons and memberships don't use the composed SKU — they send their own Analytics Tag directly (memberships fall back to the product title if untagged).
  • Capacity options and fees use their internal IDs rather than a composed SKU.
  • Untagged entities appear as - in the SKU (e.g. -AWD means the location has no Analytics Tag), which is the usual cause of odd-looking item IDs in GA4.

5. Anatomy of an item

Every ecommerce event carries an items array. Each entry describes one line of the basket — a ticket type, a capacity option, an add-on, a fee, or a membership. The fields:

FIELD MEANING
item_id The composed SKU described in section 4, e.g. SAWD. Subject to the product override and the add-on/membership/capacity special cases listed there.
item_name The ticket type ID (e.g. adult), or for add-ons the add-on and option titles, or for memberships the product name.
item_category The location title. For route (ferry/travel) products, "Origin – Destination". Memberships use MEMBER.
item_category2 The line type: Tickets, Capacity, Addons, Fees, or Memberships.
item_category3 Member or Non-Member (for tickets); Direct Debit for DD memberships.
item_category4 Y if the line includes a donation, otherwise N.
item_date_time Visit date and time, e.g. 2026-09-12T10:30.
item_product The Expian product ID.
item_venue Venue title, where the basket line has one.
price Unit price including any per-unit donation, in currency units (pounds, not pence).
quantity Number of units on the line.
currency ISO currency code, e.g. GBP.
affiliation Location title.
coupon Comma-separated promo IDs applied to the line, present only when promos apply.
item_cart_index Which basket line the item belongs to — lets GTM group tickets, capacity, and add-ons that share a booking.

6. Event reference

All payload examples below are illustrative — real values depend on the tenant's catalogue and tagging. Before every ecommerce event, the integration pushes { ecommerce: null } to clear the previous ecommerce object; this is Google's recommended practice and is expected noise when inspecting the dataLayer.

ticketing_step  —  Funnel step tracking

Fires: every time the visitor reaches or interacts with a step of the booking flow.

A lightweight breadcrumb of the visitor's progress, useful for funnel and drop-off reporting. The ticketing_step value is always Book tickets -  followed by one of:

STEP VALUE WHEN
booking flow initialised The booking page has loaded.
select type A product/experience was chosen.
select visitors The visitor is choosing ticket quantities.
select visit date A visit date was picked.
select outbound / select inbound Route (ferry/travel) journey legs are being selected.
select vehicle / select passenger Route flows: vehicle or passenger details step.
select capacity A capacity option (e.g. cabin, parking) step.
price selection outbound / price selection inbound A specific time/price was clicked in the option grid.
confirm selection The selection was confirmed.

{

"event": "ticketing_step",

"ticketing_step": "Book tickets - select visitors"

}

ecommerceMetadata  —  Visitor & basket snapshot

Fires: on page load and again after every basket change (add, remove, extras update).

A snapshot of who the visitor is and what their basket looks like, designed to be read as dataLayer variables alongside the ecommerce events. basketComposition is one of empty, ticket, shop, membership, voucher, patron, or mixed - ticket,shop style values when the basket combines types.

{

"event": "ecommerceMetadata",

"loginStatus": "Guest",

"memberStatus": "Unknown",

"basketValue": 63,

"basketComposition": "ticket"

}

view_items  —  Booking options viewed

Fires: when the grid of available times/prices is displayed to the visitor.

One item per option shown. Note this event uses a lighter item shape than basket events: the categories carry the product type, date, and time rather than the full SKU breakdown, and item_name is only populated for route products (as "Origin – Destination").

{

"event": "view_items",

"ecommerce": {

"items": [

{

"item_id": "palace-park-gardens",

"item_name": "",

"item_category": "admission",

"item_category2": "2026-09-12",

"item_category3": "10:30",

"item_category4": "",

"price": 63,

"quantity": 1,

"currency": "GBP"

}

]

}

}

add_to_cart  —  Items added to basket

Fires: when the visitor confirms a selection, adds an add-on, adds capacity, or adds a membership. Also fires with the full updated basket after extras or capacity quantities change.

The workhorse ecommerce event. value is the total of the items in the payload; items uses the full item shape from section 5 — a single confirmed selection typically produces one item per ticket type, plus items for any capacity allocation.

{

"event": "add_to_cart",

"ecommerce": {

"value": 73,

"currency": "GBP",

"items": [

{

"item_id": "PALADW",

"item_name": "adult",

"item_category": "Palace, Park & Gardens",

"item_category2": "Tickets",

"item_category3": "Non-Member",

"item_category4": "N",

"item_date_time": "2026-09-12T10:30",

"item_product": "palace-park-gardens",

"price": 28,

"quantity": 2,

"currency": "GBP",

"affiliation": "Palace, Park & Gardens",

"item_cart_index": 0

},

{

"item_id": "PALCHW",

"item_name": "child",

"item_category": "Palace, Park & Gardens",

"item_category2": "Tickets",

"item_category3": "Non-Member",

"item_category4": "N",

"item_date_time": "2026-09-12T10:30",

"item_product": "palace-park-gardens",

"price": 17,

"quantity": 1,

"currency": "GBP",

"affiliation": "Palace, Park & Gardens",

"item_cart_index": 0

}

]

}

}

Replacements: when a flow only allows one booking at a time and the visitor changes their selection, a remove_from_cart for the old items fires immediately before the add_to_cart for the new ones.

remove_from_cart  —  Items removed from basket

Fires: when the visitor removes a basket line, removes an add-on or capacity option, clears the basket, or replaces a selection (see above).

Same shape as add_to_cart — value, currency, and the removed items.

{

"event": "remove_from_cart",

"ecommerce": {

"value": 12.5,

"currency": "GBP",

"items": [

{

"item_id": "GUIDEBOOK",

"item_name": "Guidebook Standard",

"item_category": "Palace, Park & Gardens",

"item_category2": "Addons",

"price": 12.5,

"quantity": 1,

"currency": "GBP",

"item_cart_index": 0

}

]

}

}

begin_checkout  —  Checkout opened

Fires: when the checkout page loads with a basket.

This and the three events below share one shape: the full basket rendered as items (tickets, capacity, add-ons, fees, memberships — each as its own line), plus order-level totals. basketValue sits at the top level for easy variable access; transaction_id is the cart/order ID and stays constant through to purchase, and tax carries the VAT amount.

{

"event": "begin_checkout",

"basketValue": 85.5,

"ecommerce": {

"transaction_id": "1f8a2c34-9b7e-4d21-a5c3-08d1e6f7b942",

"value": 85.5,

"tax": 14.25,

"shipping": 0,

"currency": "GBP",

"coupon": "SUMMER10",

"items": [ "…full basket, per section 5…" ]

}

}

add_shipping_info  —  Customer details completed

Fires: when the visitor completes the customer details / address step of checkout.

Same shape as begin_checkout. If hashed user data capture is enabled for the tenant, this and subsequent checkout events also include a user_data object with sha256_email_address and sha256_phone_number for GA4 enhanced conversions.

add_payment_info  —  Payment step reached

Fires: when the visitor proceeds to pay.

Same shape as begin_checkout, with one addition inside ecommerce: payment_type — the payment method chosen (e.g. card, PayPal, Apple Pay, Direct Debit).

purchase  —  Order completed

Fires: on the order confirmation page once payment succeeds (fired at payment completion for Direct Debit and some hosted payment flows, where the confirmation page may not carry the order value).

The conversion event. Same shape as begin_checkout; transaction_id is the order ID GA4 uses for deduplication.

{

"event": "purchase",

"basketValue": 85.5,

"ecommerce": {

"transaction_id": "1f8a2c34-9b7e-4d21-a5c3-08d1e6f7b942",

"value": 85.5,

"tax": 14.25,

"shipping": 0,

"currency": "GBP",

"coupon": "SUMMER10",

"items": [ "…full basket, per section 5…" ]

},

"user_data": {

"sha256_email_address": "a1b2c3…",

"sha256_phone_number": "d4e5f6…"

}

}

login / sign_up  —  Account events

Fires: when a visitor signs in to, or creates, a customer account. Logging out pushes loginStatus: "Guest" without an event name.

{

"event": "login",

"loginStatus": "Logged in"

}

email_list_signup  —  Newsletter opt-in

Fires: when the visitor ticks the newsletter opt-in during checkout.

{

"event": "email_list_signup",

"method": "checkout"

}

pop_up  —  Donation & Gift Aid prompts

Fires: when the donation or Gift Aid modal appears in the booking journey, and again when the visitor answers it.

Tracks the donation/Gift Aid path for tenants that prompt for these. type is donation or gift_aid; action is appear when the modal is shown, then yes or no for the visitor's choice. A visitor who accepts the donation and is eligible for Gift Aid produces a chain: donation appear → donation yes → gift_aid appear → gift_aid yes/no.

{

"event": "pop_up",

"type": "donation",

"action": "yes"

}

7. A full booking journey, event by event

A typical admission booking (choose tickets → choose date and time → add an extra → pay) produces this sequence:

VISITOR ACTION EVENTS PUSHED
Opens the booking page ticketing_step (booking flow initialised), ecommerceMetadata
Chooses ticket quantities ticketing_step (select visitors)
Picks a date ticketing_step (select visit date)
Sees the time/price grid view_items
Clicks a time ticketing_step (price selection outbound)
Confirms the selection add_to_cart, ecommerceMetadata
Answers the donation prompt (if shown) pop_up (donation appear/yes/no, then gift_aid if eligible)
Adds a guidebook on the extras step add_to_cart (updated basket incl. the add-on), ecommerceMetadata
Opens checkout begin_checkout
Completes their details add_shipping_info (+ email_list_signup if opted in)
Clicks pay add_payment_info
Lands on confirmation purchase

If the visitor changes a confirmed selection in a single-booking flow, expect a remove_from_cart / add_to_cart pair rather than a single update event.

8. Verifying and troubleshooting

  • Confirm the container loaded. The browser console logs GTM STARTED when the container is injected, and the network tab shows a request to googletagmanager.com/gtm.js with the configured container ID.
  • Inspect the dataLayer. Type window.dataLayer in the browser console to see every event pushed so far, or use GTM's own Preview mode (Tag Assistant) for a live view with trigger evaluation.
  • No events at all? Check both switches: Google Analytics enabled on the market in Admin, and the GTM feature enabled in the tenant configuration. Remember the setting is per market — a container working on one market says nothing about another.
  • Events firing but not reaching GA4? The GTM container is missing custom-event triggers for these event names, or Consent Mode is blocking the tags because the visitor hasn't accepted analytics cookies.
  • Item IDs showing - characters? The location or ticket type involved has no Analytics Tag set — fill it in on the entity's Analytics tab in Admin.
  • Prices look wrong by ×100? They shouldn't — all monetary values in the payloads are already converted to currency units (pounds), not minor units (pence).

Was this article helpful?

That’s Great!

Thank you for your feedback

Sorry! We couldn't be helpful

Thank you for your feedback

Let us know how can we improve this article!

Select at least one of the reasons
CAPTCHA verification is required.

Feedback sent

We appreciate your effort and will try to fix the article