← All posts

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.

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}}

  1. 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

  1. 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)

  1. 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 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:

Found this useful?

That's the kind of thinking I bring to every account I work on. If your Google Ads need a pair of senior eyes, the call is free and I respond personally within 24 hours.

Book a free 30-min call Or get a free account audit