# How to Connect Cliniko to GA4 Using Google Tag Manager

> A step-by-step guide to connecting Cliniko to GA4 using GTM. Tracks bookings, cancellations, service name, appointment date, and value, with code examples and webhook setup.

**By Murtaza Rangwala** · **Published:** May 18, 2026 · **Read time:** 10 min read · **Category:** GA4

Cliniko is great at running a clinic. It is honestly not great at telling you which marketing channels are driving bookings.

The default setup gives you almost nothing in GA4. No service name. No appointment value. No way to see whether the booking came from Google Ads, Meta, or a referral. Cancellations are completely invisible.

I've spent more hours than I'd like to admit fixing this for clinic clients. The good news: once you set it up properly, you get clean, channel-attributed booking data in GA4, including service-level and appointment-date detail. Cancellations included.

Here's the full setup, step by step.

!!!
**The TL;DR:** Cliniko doesn't natively talk to GA4. You bridge them using **GTM on the confirmation page** (for booking events), **cross-domain tracking** (to preserve attribution between your site and `*.cliniko.com`), and **webhooks + Measurement Protocol** (for cancellations).
!!!

## What you'll be able to track once this is done

- **Booking started** — visitor opens the booking widget.
- **Service selected** — service_name pushed into the data layer.
- **Booking confirmed** — with service_name, appointment_date, practitioner, and value.
- **Booking cancelled** — fired server-side from a Cliniko webhook.
- **Channel attribution** — preserved across the hop to bookings.cliniko.com.
- **Conversion value** — passed cleanly into GA4 for Google Ads Smart Bidding.

That's the full loop. Marketing spend → booking → cancellation, all attributed and segmentable.

## Prerequisites

Before you start, make sure you have:

- A **GA4 property** with a configured data stream.
- A **GTM container** installed on your main website.
- **Admin access** to your Cliniko account.
- A **small server endpoint** to handle webhooks (Cloud Functions, Vercel, AWS Lambda, anything that can receive a POST).
- Your **Cliniko API key** (only for the server-side cancellation tracking).
- Your **GA4 Measurement ID** and an **API Secret** (created under Admin > Data Streams > Measurement Protocol API secrets).

## How Cliniko handles bookings (the part that makes this awkward)

Before the setup, it helps to understand the constraint:

- Your main website lives on `yourdomain.com`.
- The Cliniko booking widget lives on a subdomain like `yourpractice.au2.cliniko.com` or `bookings.cliniko.com`.
- When a visitor clicks **"Book now,"** they leave your domain and arrive on Cliniko's hosted page.
- GA4's default cross-domain tracking does not include `cliniko.com`.
- Cliniko allows custom scripts on the booking confirmation page, but template variables and access depend on your plan.

This is why the setup needs multiple pieces. There is no single tag that does the whole job.

## Step 1: Configure cross-domain tracking between your site and Cliniko

In GA4:

1. Open **Admin > Data Streams**.
2. Click your web stream.
3. Click **Configure tag settings > Configure your domains**.
4. Add both:
   - `yourdomain.com`
   - `cliniko.com` (or your specific Cliniko subdomain)

This makes GA4 stitch sessions together across the domain hop. You'll start seeing a `_gl` parameter appended to the URL when visitors move from your main site to the Cliniko booking page.

> If you skip this step, every Cliniko booking will look like a "direct" or "referral" visit. Your Google Ads, Meta, and SEO attribution will all be wrong.

## Step 2: Install GTM on the Cliniko confirmation page

Cliniko lets you add custom HTML to the page that appears after a booking is completed. This is where you fire your booking conversion.

In Cliniko:

1. Go to **Settings > Online Bookings > Confirmation page**.
2. Paste your GTM container snippet (same one from your main site, both the `<head>` and `<body>` portions).
3. Below the GTM snippet, add a small script that pushes booking details to the data layer (see Step 3).

If your Cliniko plan doesn't allow custom scripts on the confirmation page, the fallback is to redirect to a thank-you page on your own domain after booking and fire the data layer event there.

## Step 3: Push booking data into the data layer

This is where the **service name**, **appointment date**, **practitioner**, and **value** get sent.

On the confirmation page, drop this script. Replace the placeholders with whatever template variables Cliniko exposes in your account:

```html
<script>
  window.dataLayer = window.dataLayer || [];
  window.dataLayer.push({
    event: 'cliniko_booking_confirmed',
    booking_id: '{{appointment_id}}',
    service_name: '{{appointment_type_name}}',
    practitioner_name: '{{practitioner_name}}',
    appointment_date: '{{appointment_date}}',
    appointment_time: '{{appointment_time}}',
    location: '{{business_name}}',
    value: {{appointment_price}},
    currency: 'AUD'
  });
</script>
```

!!!
**Heads up:** Cliniko's available template variables vary by plan and account configuration. Confirm yours under **Settings > Online Bookings > Confirmation page** before copying this verbatim. Where Cliniko doesn't expose a variable directly, you can usually pull it via their API in a small server function.
!!!

## Step 4: Build the GTM trigger, variables, and tag

In GTM:

### Create the custom event trigger

1. **Triggers > New**.
2. Trigger Type: **Custom Event**.
3. Event name: `cliniko_booking_confirmed`.
4. Save as `Trigger - Cliniko Booking Confirmed`.

### Create five data layer variables

For each of the keys you pushed, create a Data Layer Variable:

- `dlv_service_name` → reads `service_name`
- `dlv_practitioner_name` → reads `practitioner_name`
- `dlv_appointment_date` → reads `appointment_date`
- `dlv_appointment_time` → reads `appointment_time`
- `dlv_booking_id` → reads `booking_id`
- `dlv_value` → reads `value`
- `dlv_currency` → reads `currency`

### Create the GA4 Event tag

1. **Tags > New > GA4 Event**.
2. **Configuration tag:** your existing GA4 config tag.
3. **Event name:** `booking_confirmed`.
4. **Event parameters:**
   - `service_name` = `{{dlv_service_name}}`
   - `practitioner` = `{{dlv_practitioner_name}}`
   - `appointment_date` = `{{dlv_appointment_date}}`
   - `appointment_time` = `{{dlv_appointment_time}}`
   - `booking_id` = `{{dlv_booking_id}}`
   - `value` = `{{dlv_value}}`
   - `currency` = `{{dlv_currency}}`
5. **Trigger:** `Trigger - Cliniko Booking Confirmed`.

Save and publish the container.

## Step 5: Register the custom dimensions in GA4

GA4 will collect your custom parameters but won't show them in reports until they're registered as custom dimensions.

1. **Admin > Custom definitions > Custom dimensions**.
2. Add new event-scoped dimensions for:
   - `service_name`
   - `practitioner`
   - `appointment_date`
   - `appointment_time`
   - `booking_id`
3. Click **Save**.

Wait up to 24 hours for these to populate in standard reports. Explorations show them sooner.

## Step 6: Mark booking_confirmed as a key event

1. **Admin > Events**.
2. Find `booking_confirmed`.
3. Toggle **Mark as key event** (formerly "conversion").

Now you can import this into Google Ads under **Tools > Conversions > Import from GA4** and use it as the conversion action for Smart Bidding.

## Step 7: Track cancellations with Cliniko webhooks + Measurement Protocol

This is the part most setups skip. Cancellation tracking matters because:

- Smart Bidding optimises on conversions. If your cancellation rate is 20%, you're inflating ROAS by 20%.
- Channel comparisons are useless if cancellation rates differ wildly by source.
- LTV modelling needs real, completed bookings — not just confirmed ones.

### Set up the webhook in Cliniko

1. In Cliniko, go to **Settings > Integrations > Webhooks**.
2. Create a new webhook listening for:
   - `appointment.cancelled`
   - `appointment.deleted` (optional, captures hard deletes)
3. Point it at your endpoint, e.g. `https://yourdomain.com/api/cliniko-webhook`.

### Handle the webhook on your server

Your endpoint receives the cancelled appointment payload. Forward it to GA4 using the **Measurement Protocol**:

```javascript
// Node.js / Vercel / Cloud Functions — adapt to your stack
const MEASUREMENT_ID = 'G-XXXXXXX';
const API_SECRET = process.env.GA4_API_SECRET;

export default async function handler(req, res) {
  const payload = req.body;

  // Look up the original client_id you stored at booking time
  const clientId = await lookupClientId(payload.appointment_id);

  await fetch(
    `https://www.google-analytics.com/mp/collect?measurement_id=${MEASUREMENT_ID}&api_secret=${API_SECRET}`,
    {
      method: 'POST',
      body: JSON.stringify({
        client_id: clientId || `cliniko.${payload.appointment_id}`,
        events: [{
          name: 'booking_cancelled',
          params: {
            booking_id: payload.appointment_id,
            service_name: payload.appointment_type_name,
            cancellation_reason: payload.cancellation_note || 'unknown',
            value: -Math.abs(payload.appointment_price),
            currency: 'AUD'
          }
        }]
      })
    }
  );

  res.status(200).end();
}
```

> The negative `value` (`-Math.abs(...)`) is intentional. If you're using value-based Smart Bidding, the cancellation pulls the revenue back out, giving the algorithm a more accurate picture.

### About client_id matching

For GA4 to attribute the cancellation to the original session and channel, you need the **same client_id** that was used at booking time.

The cleanest way:

1. At booking confirmation (Step 3), also capture the GA4 client_id into your data layer using `gtag('get', 'G-XXXXXXX', 'client_id', callback)` or by reading the `_ga` cookie.
2. Send it to your server as part of the booking metadata.
3. Store it against the booking ID in your database.
4. Retrieve it when the webhook fires.

If you can't link them, you'll still see cancellation counts in GA4, but channel attribution for those cancellations will be lost.

Google's [Measurement Protocol reference](https://developers.google.com/analytics/devguides/collection/protocol/ga4) covers the payload structure in detail.

## Step 8: Test everything before going live

The number of accounts I've audited where someone "set up Cliniko tracking" and never verified it actually works is embarrassing. Don't skip this.

Run these checks end-to-end:

1. **GTM Preview Mode** — open Preview, complete a test booking, confirm the `cliniko_booking_confirmed` data layer event fires with all parameters populated.
2. **GA4 DebugView** — enable Debug Mode and make a test booking. Confirm `booking_confirmed` appears with `service_name`, `appointment_date`, `value`, and `currency`.
3. **Cross-domain check** — start a session on your main site, click through to Cliniko, complete a booking. In GA4 Realtime, the user should show as **one session**, not two.
4. **Webhook test** — cancel the test booking in Cliniko. Confirm `booking_cancelled` shows up in GA4 DebugView within a minute, with the negative value.
5. **Google Ads import** — once `booking_confirmed` is marked as a key event, import it into Google Ads and confirm it appears under **Tools > Conversions**.

Repeat with at least three different services so you can confirm `service_name` populates correctly for each.

## Use case: a multi-location physio clinic

A composite based on patterns from multiple clinic implementations.

A physio practice with three locations was running Google Ads with "form submission" as the conversion action. They had no idea which campaigns were producing actual booked appointments versus which were just collecting tire-kickers.

**Their setup before:**

- GTM installed on the main site only.
- Generic "form submit" tracked as conversion, no service breakdown.
- Cliniko bookings invisible in GA4.
- Cancellations tracked in their finance system but never sent back to GA4.

We rolled out the full setup above. Within four weeks:

- **41% of "form submissions"** being credited as conversions had no matching booking in Cliniko. Google Ads was optimising toward junk leads.
- A specific **Performance Max campaign** that looked like the top performer was actually generating the highest cancellation rate at **34%** vs an 11% account average.
- The reception team was wasting an estimated **7 hours per week** chasing no-show appointments driven by that one campaign.

After re-pointing Smart Bidding at `booking_confirmed` with cancellations subtracting value:

- **Cost per real booking dropped 38%** over six weeks.
- The high-cancellation PMax campaign was paused. Its budget went into Search campaigns targeting specific service queries like *"sports physio Sydney"* and *"women's health physio Bondi."*
- Service-level data in GA4 revealed that two services had near-zero ROAS, prompting a pricing review.

None of these insights existed before. The data was always there. It just wasn't connected.

## Common mistakes and how to avoid them

- **Sending `order_total` without subtracting cancellations.** Inflates ROAS. Use the negative-value Measurement Protocol approach above.
- **Tracking both "form submit" and "booking confirmed" as conversions.** Double-counts everything. Pick one (the booking) and turn the other off.
- **Forgetting to add Cliniko's domain to cross-domain tracking.** Kills channel attribution. This is the single most common mistake.
- **Not registering custom dimensions in GA4.** The events come through with full data, but `service_name` will be invisible in reports.
- **Hardcoding the currency.** If your clinic accepts multiple currencies, pull it from Cliniko's data dynamically.
- **Skipping the test bookings.** Always do at least three end-to-end test bookings before going live, and cancel one. Then check Search > "test" or use a test email to clean them up afterwards.

## Bottom line

Cliniko doesn't have a one-click GA4 integration. That doesn't mean your clinic marketing has to fly blind.

- Use **GTM on the confirmation page** for booking events.
- Use **cross-domain tracking** to preserve channel attribution.
- Use **webhooks + Measurement Protocol** for cancellations.
- Push **service_name** and **appointment_date** in the data layer so you can segment performance by service.

The whole setup takes a day if you have a developer. Less if you've done it before. The attribution clarity it gives Google Ads, Meta, and SEO is permanent.

Stop running blind. Connect Cliniko properly.

---

**Sources and further reading:**

- [Cliniko Webhooks documentation](https://docs.cliniko.com/en/articles/2017733-webhooks)
- [GA4 Measurement Protocol reference](https://developers.google.com/analytics/devguides/collection/protocol/ga4)
- [GA4 cross-domain measurement (Google Help)](https://support.google.com/analytics/answer/10071811)
- [GTM custom event trigger (Google Help)](https://support.google.com/tagmanager/answer/7679219)
- [GA4 custom dimensions and metrics (Google Help)](https://support.google.com/analytics/answer/10075209)

---

**Tags:** ga4, gtm, google tag manager

## About the author

Murtaza Rangwala is a senior independent Google Ads consultant. 8 years, 1,900+ campaigns shipped, $250M+ in client revenue generated. Independent practice capped at four concurrent clients.

- More posts: https://www.murtazarangwala.com/blog
- Book a 30-min call: https://calendly.com/murtaza_rangwala/30min
- Free Google Ads audit: https://www.murtazarangwala.com/#audit