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.comorbookings.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:
- Open Admin > Data Streams.
- Click your web stream.
- Click Configure tag settings > Configure your domains.
- 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:
- Go to Settings > Online Bookings > Confirmation page.
- Paste your GTM container snippet (same one from your main site, both the
<head>and<body>portions). - 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
- Triggers > New.
- Trigger Type: Custom Event.
- Event name:
cliniko_booking_confirmed. - 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→ readsservice_namedlv_practitioner_name→ readspractitioner_namedlv_appointment_date→ readsappointment_datedlv_appointment_time→ readsappointment_timedlv_booking_id→ readsbooking_iddlv_value→ readsvaluedlv_currency→ readscurrency
Create the GA4 Event tag
- Tags > New > GA4 Event.
- Configuration tag: your existing GA4 config tag.
- Event name:
booking_confirmed. - 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}}
- 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.
- Admin > Custom definitions > Custom dimensions.
- Add new event-scoped dimensions for:
- service_name - practitioner - appointment_date - appointment_time - booking_id
- 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
- Admin > Events.
- Find
booking_confirmed. - 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
- In Cliniko, go to Settings > Integrations > Webhooks.
- Create a new webhook listening for:
- appointment.cancelled - appointment.deleted (optional, captures hard deletes)
- 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 negativevalue(-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:
- 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_gacookie. - Send it to your server as part of the booking metadata.
- Store it against the booking ID in your database.
- 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:
- GTM Preview Mode — open Preview, complete a test booking, confirm the
cliniko_booking_confirmeddata layer event fires with all parameters populated. - GA4 DebugView — enable Debug Mode and make a test booking. Confirm
booking_confirmedappears withservice_name,appointment_date,value, andcurrency. - 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.
- Webhook test — cancel the test booking in Cliniko. Confirm
booking_cancelledshows up in GA4 DebugView within a minute, with the negative value. - Google Ads import — once
booking_confirmedis 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_totalwithout 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_namewill 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:
