Custom install

TrueMetriks tracks your pageviews automatically and lets you fire checkouts, leads, schedules, and purchases straight from your own code with one head snippet. Here is how to set it up.

What gets tracked

  • PageView on every page load and SPA route change (automatic)
  • InitiateCheckout when you call tmFireEvent("InitiateCheckout", ...) (manual - from your checkout page or step)
  • Lead when you call tmFireEvent("Lead", ...) (manual - from your form handlers)
  • Schedule when you call tmFireEvent("Schedule", ...) (manual - from your booking confirmations)
  • CompleteRegistration when you call tmFireEvent("CompleteRegistration", ...) (manual - from your signup handler, custom installs only)
  • Purchase when you set window.tmOrder on your thank-you page, or fire tmFireEvent("Purchase", ...) per charge in an upsell funnel
  • Anything else you want - video views, button clicks, tab opens, quiz answers - when you call tmFireEvent("YourEventName", ...) with a name of your own (see Custom events)

InitiateCheckout, Lead, Schedule, and Purchase are forwarded to Facebook, Google Ads, TikTok, and GA4. Custom events are sent to Facebook, TikTok and GA4 automatically, but not to Google Ads.

Video guide

How to install TrueMetriks tracking on a custom website

Set this up with AI

Most custom installs go faster with an AI pair. Copy the prompt below and paste it into Claude / ChatGPT / Cursor / etc. along with the relevant files from your codebase. The AI will ask you the follow-up questions it needs, then write the integration for you.

I want to install TrueMetriks tracking on my website. TrueMetriks is an analytics
+ ad-platform fanout tracker - one install fires events to my dashboard AND to
every ad platform I have connected (Facebook, Google Ads, TikTok, GA4).

The install has two parts:

1. INSTALL SNIPPET (one line, in <head> on every page):
   <script async src="https://MY-CNAME/t/s-custom.js"></script>
   Replace MY-CNAME with the CNAME hostname shown on my TrueMetriks install card.

2. AT RUNTIME, the snippet exposes three globals:

   window.tmFireEvent(name, customData, userData)
     - Fires any event. Standard names: "Lead", "InitiateCheckout", "Schedule",
       "Purchase", "CompleteRegistration", "Identify". Any other string is
       a custom event (see OPTIONAL: CUSTOM EVENTS below).
     - customData: optional object with event-specific custom payload.
       For Purchase: { value, currency, order_id, content_ids, num_items, items }.
       For InitiateCheckout: { value, currency, content_ids, num_items }, value
       in DOLLARS, all optional.
       For Lead/Schedule/custom: optional context, e.g. { source: "newsletter" }.
       For CompleteRegistration: all optional - { content_name, status, value,
       currency }; send currency only if you send value.
     - userData: optional object with PII for ad-platform match quality:
       { email, first_name, last_name, phone, city, state, zip, country,
         dob, gender, externalId }. Hashed server-side before fanout.

   window.tmIdentify(email, traits)
     - Persists the visitor's identity for the session so every subsequent
       event carries email + traits to ad-platform fanout.
     - traits: { first_name, last_name, phone, city, state, zip, country,
                 dob, gender, externalId, address } - all optional.
     - Call once after you know the visitor's email (form submit, login, etc).

   window.tmOrder = { order_id, email, currency, items }
     - Set this object on the thank-you / order-confirmation page. The tracker
       polls window.tmOrder every 500ms for up to 30 seconds and auto-fires
       Purchase + Identify the moment it appears.
     - order_id (required, unique string), email (recommended), currency
       (defaults to "USD"), items (required array): each item is
       { sku, name, price (in CENTS), qty, category?, brand?, variant?, image_url? }.
     - Same-page upsells: mutate order_id + items after each upsell. The tracker
       keeps polling and dedups by order_id in sessionStorage.

3. AUTOMATIC BEHAVIOUR (you do NOT call these manually):
   - PageView on every load and SPA route change
   - First-party session cookie on the CNAME domain
   - utm_*, gclid, fbclid, ttclid, wbraid, gbraid capture on first touch

4. OPTIONAL: CUSTOM EVENTS (only if I ask for them):
   - Beyond the standard events, I can track ANY action my own code can detect
     by calling tmFireEvent with a name of my choosing, for example:
       window.tmFireEvent("VideoProgress", { video: "sales-vsl", percent: 50 });
       window.tmFireEvent("PricingViewed", { plan: "annual" });
       window.tmFireEvent("DemoButtonClicked", { location: "hero" });
   - Names: short PascalCase, letters and numbers only, no spaces, and never
     reuse a standard name. Keep a name stable once my ads use it.
   - Fire each custom event once per meaningful action (for example once per
     video milestone, not on every timeupdate tick). Leave userData out; the
     tracker attaches the visitor's identity by itself.
   - Custom events show in my TrueMetriks dashboard and go to Facebook and
     TikTok under the same name and to GA4 in snake_case. They are NOT sent to
     Google Ads. Facebook and TikTok can each be switched off per pixel under
     Settings > Tracking > Facebook or TikTok > Your own events.

WHAT I WANT YOU TO DO:
- Do the MAIN install first. It is required and covers: the snippet,
  InitiateCheckout, Lead, Schedule, CompleteRegistration (if I have signups),
  and Purchase.
- InitiateCheckout does NOT fire by itself on a custom install and there is no
  Checkout URL pattern setting for it. YOU must detect when my checkout page
  or step has loaded, using whatever fits my codebase best: the URL or route
  (for example a path containing /checkout), a page template, tag, body class
  or data attribute my checkout pages carry, the checkout component mounting,
  or the checkout step opening in a single-page flow. Then fire
  tmFireEvent("InitiateCheckout", { value, currency }) once per checkout visit
  (value in DOLLARS), never on every re-render or route event.
- Ask me: (a) what framework my site is built on (plain HTML, Next.js, Vite,
  CRA, Vue/Nuxt, Astro, etc.), (b) which forms, bookings and signups should fire
  Lead, Schedule and CompleteRegistration, and from where (which form, which
  route, which button), (c) which page, route or step is my checkout, (d) which
  fields on my thank-you page hold the order_id, email, currency, and line items
  so you can write the tmOrder assignment correctly.
- Ask me whether this is a single-checkout-into-one-thank-you-page funnel or a
  multi-step / upsell funnel (a checkout page, then one or more upsell or
  downsell pages, each its own charge). For a single thank-you page, use one
  window.tmOrder on the thank-you page. For a multi-step / upsell funnel, fire
  Purchase per charge with tmFireEvent("Purchase", { value, currency, order_id })
  from each charge-success handler instead - value in DOLLARS, and each charge's
  own payment id (Stripe pi_..., PayPal capture id) as order_id - so no sale is
  lost if the buyer leaves before the final thank-you page. Never use both
  methods for the same order.
- Then write the integration: where the snippet goes in my codebase, the
  exact code for each event call, and where to drop window.tmOrder on the
  thank-you page. Use the framework-idiomatic mechanism (next/script,
  react-helmet, useHead, etc.) - do not put raw <script> tags into JSX.
- Once the main install is written, ask me (e) whether I want to track
  anything else as a custom event, such as video engagement, button clicks,
  pricing or tab views, or quiz steps. Only if I say yes, write those
  tmFireEvent calls following part 4. If I say no, skip custom events.
- For TypeScript projects, add the `declare global` window typings.
- Do NOT use the deprecated `tmq.push(...)` API - it does not exist on the
  current tracker. The three globals above are the entire API surface.

Paste that prompt, answer the AI's questions, and you should have a working integration in under fifteen minutes. The manual reference below covers the same ground for readers who want to wire it by hand.

Manual install

1. Copy the tracker snippet from your dashboard

In your TrueMetriks dashboard, open Settings > Tracking, pick the Custom tile, and click Copy on the highlighted Tracker snippet:

Showing setup for Custom

Custom install

For any site you can edit the HTML on. Paste this snippet, then fire events from your own code.

1 TRACKER SNIPPET

HTML
<script async src="https://q.yourdomain.com/t/s-custom.js"></script>

Paste before </head> on every page you want tracked.

Important: read the guide on how to set up Custom and how to prepare the data you send for each event (checkout, lead, book a call, purchase, etc.).

Settings → Tracking → Custom - copy the highlighted Tracker snippet, then paste it into your site head.

The snippet you copy looks like this (with your own CNAME hostname filled in):

<script async src="https://YOUR-CNAME/t/s-custom.js"></script>

YOUR-CNAME is the first-party hostname you set up at CNAME setup (for example t.yourdomain.com).

2. Paste the snippet in your site head

Paste it just before </head> on every page you want tracked. The snippet is async, so it loads in the background without blocking your page render.

If you use a framework, see Framework notes below for the idiomatic way to inject it.

3. Fire InitiateCheckout from your checkout step

A custom install does not fire InitiateCheckout by itself, and there is no Checkout URL pattern to set (that field only exists for GoHighLevel and ClickFunnels). Your code detects when the checkout page or step has loaded, in whatever way suits your site, and fires it once. See InitiateCheckout below for the ways to detect it and the code.

What runs automatically

Once the snippet loads, the tracker handles these on its own - you never call any of them manually:

  • Pageview on the initial load and on every SPA route change.
  • First-party session cookie on the CNAME domain.
  • Click-ID capture on first touch: utm_*, gclid, fbclid, ttclid, wbraid, gbraid.
  • Form-field capture of email, phone, first name, and last name as visitors type, so every subsequent event carries identity for ad-platform match quality even before you call tmIdentify.
  • Client IP, user agent, timezone resolved server-side.

You only have to write code for events the tracker cannot see on its own: checkouts, leads, schedules, and purchases, plus any custom events you choose to add.

The tracker API

The snippet exposes exactly three globals. That is the complete public API.

window.tmFireEvent(name, customData, userData)

Fires any event. Standard event names are Lead, InitiateCheckout, Schedule, CompleteRegistration, Purchase, Identify. Any other name is a custom event.

window.tmFireEvent(
  "Lead",                                       // 1. event name
  { source: "newsletter_popup" },               // 2. customData - optional context
  { email: "[email protected]",                  // 3. userData - PII for ad-platform match
    first_name: "Jane",
    last_name: "Doe",
    phone: "+15551234567" }
);
  • name (string, required) - standard name or your own custom name
  • customData (object, optional) - event-specific context. For Purchase this carries value, currency, order_id, content_ids, num_items, items[]. For other events, anything you want to attach.
  • userData (object, optional) - PII for ad-platform match quality. Fields: email, first_name, last_name, phone, city, state, zip, country, dob, gender, externalId. Hashed server-side before fanout.

If you have already called tmIdentify, you can pass null for userData - the tracker remembers the identity from _tmIdentity and applies it automatically.

window.tmIdentify(email, traits)

Persists the visitor's identity in sessionStorage so every subsequent event in the session carries the same PII bundle to ad-platform fanout - even if you forget to pass userData on later tmFireEvent calls.

window.tmIdentify("[email protected]", {
  first_name: "Jane",
  last_name: "Doe",
  phone: "+15551234567"
  // Optional: city, state, zip, country, dob, gender, externalId, address
});

Call it once when you first know the email (right after opt-in success, login success, or a checkout-step email field is filled). The tracker also auto-stitches identity from the form-field interceptor (see What runs automatically) so explicit calls are belt-and-braces.

Calling this early also matters for conversions that reach us by webhook rather than from the browser. An email we have already seen on a visit is what lets a webhook conversion find that person, so capture it as soon as you have it. For the exact way a webhook conversion is matched to a visitor, and the hidden-field snippet that makes it exact, see Make sure the conversion lands on the right visitor.

Two ways to fire Purchase - pick the one that matches your funnel:

  • One checkout into one thank-you page (most stores): set window.tmOrder on the thank-you page. Simplest - covered below.
  • A multi-step or upsell funnel (a checkout page, then one or more upsell / downsell pages, each its own charge): fire a Purchase from your code the moment each charge succeeds, with window.tmFireEvent("Purchase", ...). This captures every sale the instant it happens, so a buyer who pays and then leaves before the final thank-you page is still tracked. See Purchases in upsell funnels.

Never use both methods for the same order, or it will be counted twice.

window.tmOrder =

Set this object on your thank-you / order-confirmation page. The tracker polls window.tmOrder every 500ms for up to 30 seconds and the moment it appears, fires Purchase + Identify with the order's contents.

window.tmOrder = {
  order_id: "ORD-12345",      // REQUIRED, unique per order
  email: "[email protected]", // recommended, improves match quality
  currency: "USD",            // optional, defaults to USD
  items: [
    {
      sku:   "SKU-1",          // recommended
      name:  "Product A",      // recommended
      price: 4900,             // REQUIRED, in CENTS (4900 = $49.00)
      qty:   1                 // optional, defaults to 1
      // Optional: category, brand, variant, image_url
    },
    { sku: "SKU-2", name: "Product B", price: 2900, qty: 2 }
  ]
};

The tracker computes the total from price * qty summed across items (divided by 100 to get the dollar value), dedups by order_id in sessionStorage, and fires once. Refreshes of the thank-you page do not double-fire.

Each items[].price here is in cents. If instead you fire Purchase directly with window.tmFireEvent("Purchase", { value }) (see Purchases in upsell funnels), that value is in dollars, not cents.

Same-page upsells. If your checkout offers post-purchase upsells on the same page, mutate window.tmOrder.order_id and window.tmOrder.items after each upsell completes. The tracker keeps polling and fires once per unique order_id.

// First charge succeeded
window.tmOrder = { order_id: "ORD-12345", email: "...", items: [{ ... }] };

// User accepted upsell - new order fires
window.tmOrder.order_id = "ORD-12345-UP1";
window.tmOrder.items = [{ sku: "UPSELL-1", name: "Bonus Pack", price: 1900, qty: 1 }];

Purchases in upsell funnels

If your funnel charges in steps - a main checkout, then post-purchase upsells or downsells, each on its own page or as its own charge - do not wait for the thank-you page to report the sale. Fire a Purchase from your charge-success handler the moment each charge clears, so no sale is lost if the buyer drops off mid-funnel.

Call window.tmFireEvent("Purchase", ...) at every successful charge - the main checkout and each upsell - using that charge's own payment id as order_id:

// Run this the instant a charge succeeds (main checkout, each upsell, each downsell):
window.tmFireEvent("Purchase",
  {
    value: 47.00,                 // DOLLARS - this charge's amount (NOT cents)
    currency: "USD",
    order_id: paymentId           // unique per charge, e.g. the Stripe pi_... id
  },
  { email: buyerEmail }           // pass first_name / last_name / phone too if you have them
);
  • value is in dollars here (47.00) - unlike window.tmOrder.items[].price, which is in cents. This is the one place the two purchase methods differ.
  • order_id must be unique per charge. Use your payment processor's transaction id (Stripe pi_..., PayPal capture id). The tracker dedups on it, so a refresh or retry will not double-count, and each charge is recorded as its own sale.
  • Fire it the moment success is confirmed, before or together with any redirect to the next step - the event is sent with a keepalive request, so it survives the page change.

Example - a checkout handler and an upsell handler:

// Main checkout charge succeeded
function onCheckoutPaid(charge) {
  window.tmFireEvent("Purchase",
    { value: 97.00, currency: "USD", order_id: charge.id },
    { email: customer.email, first_name: customer.firstName });
  // ...then send them to the first upsell
}

// Upsell accepted and charged
function onUpsellPaid(charge) {
  window.tmFireEvent("Purchase",
    { value: 297.00, currency: "USD", order_id: charge.id },
    { email: customer.email });
  // ...then send them to the next step
}

Do not also set window.tmOrder for these orders - per-charge firing replaces it. Using both reports each sale twice.

Standard events

Event name When to fire Who fires it
PageView Every page load and SPA route change Automatic
InitiateCheckout Visitor reaches your checkout step You, from your checkout page or step
Lead Visitor submits an opt-in form You, from your form success handler
Schedule Visitor confirms a calendar booking You, from your booking success handler
CompleteRegistration Visitor completes a signup or registration You, from your signup success handler
Purchase Order is successfully paid Automatic via window.tmOrder, or you, per charge via tmFireEvent in upsell funnels
Identify First time you know the visitor's email Automatic (via window.tmIdentify or form interceptor)

InitiateCheckout

Fired once when your checkout page or step loads. Nothing fires it for you on a custom install, so without this call your checkouts are not tracked and not sent to your ad platforms:

window.tmFireEvent("InitiateCheckout", {
  value: 49.00,                 // optional - DOLLARS (NOT cents), the cart or plan price
  currency: "USD",              // optional - send it if you send value
  content_ids: ["pro-monthly"], // optional - the product or plan ids
  num_items: 1                  // optional
});

How you detect the checkout is up to you. Use whatever your site already knows:

  • The URL or route, for example if (location.pathname.includes("/checkout")).
  • A marker on the page, such as a template name, body class or data attribute your checkout pages carry.
  • Your checkout component mounting, in React, Vue or another framework.
  • The checkout step opening, if checkout is a step inside a single-page flow.

Fire it once per checkout visit, not on every re-render (in React, from a useEffect with an empty dependency list). Every field is optional; window.tmFireEvent("InitiateCheckout") on its own also works. If the visitor's email is already known, pass it as the third argument, the same as for Lead.

Lead

Fired from your opt-in form's success handler. Pass user data so the ad platforms can match the lead to a known person:

window.tmFireEvent("Lead", { source: "homepage_hero_form" }, {
  email: "[email protected]",
  first_name: "Jane",
  last_name: "Doe",
  phone: "+15551234567"
});

Schedule

Fired from your booking-confirmation handler (Calendly redirect handler, custom calendar webhook, etc.):

window.tmFireEvent("Schedule", { appointment_id: "abc-123" }, {
  email: "[email protected]",
  first_name: "Jane",
  last_name: "Doe",
  phone: "+15551234567"
});

CompleteRegistration

Fired when someone finishes a signup: an account is created, a membership form is submitted, a free trial is started, a course enrolment completes. Call it from the handler that runs once the registration has actually succeeded, not on form submit.

window.tmFireEvent("CompleteRegistration", {
  content_name: "Free account",   // optional - which signup this was
  status: true,                   // optional - registration completed
  value: 0,                       // optional - what a signup is worth to you
  currency: "USD"                 // optional - required only if you send value
}, {
  email: "[email protected]",
  first_name: "Jane",
  last_name: "Doe",
  phone: "+15551234567"
});

Every field in the first object is optional, including value and currency. Send value only if a signup has a meaningful worth to you and you want Facebook to optimise toward it; if you do send it, send currency with it. status is a boolean meaning the registration completed, and content_name labels which signup it was when you have more than one.

The second object is the same identity bundle as Lead, and it is the half that matters for matching. The tracker adds the rest on its own: the Facebook click id (_fbc) and browser id (_fbp), the visitor's IP and user agent, and the TrueMetriks visitor id, all on top of whatever email, name and phone you pass. Everything personal is hashed before it reaches an ad platform. If you already called tmIdentify, the stored identity is merged in automatically and you can leave the third argument out.

There is nothing to switch on. CompleteRegistration is not one of the seven events every install gets, because most platforms cannot send it. So it stays out of the way until your site fires one, and then it simply works: the event is forwarded to your connected pixels from the first fire, and a new checkbox appears under Settings > Integrations > Facebook > Allowed events already ticked, so you can see it and switch it off if you ever want to. Firing it from your code is the whole setup.

Custom events

The standard events cover leads, bookings, signups and sales. A custom event is everything else: any action on your site that your own code can detect, sent under a name you choose.

With a custom install you can track almost anything. A few ideas:

  • How far visitors watch your sales video (25%, 50%, 75%, finished)
  • Clicks on a specific button, like "Book a demo" or "See pricing"
  • A visitor reaching your pricing table or testimonials section
  • Each step of a quiz, calculator or multi-step form
  • File downloads, chat widget opens, coupon code copies

How it works

Call window.tmFireEvent with your own event name, plus an optional object of details:

window.tmFireEvent("PricingViewed", { plan: "annual" });

That single call does four things:

  1. Records it in TrueMetriks. Within a few minutes it appears in your dashboard: open Conversions, click the events dropdown (it reads All events) and pick it under Custom events, to see how many people did it and the journeys that led there.
  2. Sends it to Facebook as a custom event under the same name, on every pixel you have connected. You can build audiences from it and use it for custom conversions in Ads Manager. To stop this, switch off Your own events on a pixel in the Facebook card.
  3. Sends it to TikTok as a custom event under the same name, on every pixel whose Your own events switch is On. It starts On unless you have changed the Allowed events list on that pixel. You can switch it off per pixel in the TikTok card.
  4. Sends it to GA4 in snake_case (PricingViewed becomes pricing_viewed).

Custom events are not sent to Google Ads. Google Ads receives only the standard events.

The visitor's identity (email, click IDs, cookies) is attached automatically, so you do not need to pass the third userData argument unless you know the visitor's details at that moment.

Naming tips

  • Use short PascalCase names with letters and numbers only, like VideoProgress or DemoButtonClicked. No spaces.
  • Do not reuse a standard name (Lead, Purchase and so on) for something else.
  • Keep a name stable once your ads or reports use it. Renaming creates a new, separate event.
  • Fire once per meaningful action. Sending an event on every scroll tick or every second of a video floods your reports without telling you more.

The tracker loads async, so wrap your calls in a small helper that checks it is ready:

function tmTrack(name, details) {
  if (window.tmFireEvent) window.tmFireEvent(name, details);
}

The examples below use this helper.

Example: video engagement

Fires once when the video starts, at 25%, 50% and 75%, and when it finishes. Add data-tm-video="sales-vsl" to any <video> tag you want tracked:

document.querySelectorAll("video[data-tm-video]").forEach(function (video) {
  var name = video.dataset.tmVideo;
  var sent = {};

  video.addEventListener("play", function () {
    if (sent.start) return;
    sent.start = true;
    tmTrack("VideoStarted", { video: name });
  });

  video.addEventListener("timeupdate", function () {
    if (!video.duration) return;
    var percent = (video.currentTime / video.duration) * 100;
    [25, 50, 75].forEach(function (mark) {
      if (percent >= mark && !sent[mark]) {
        sent[mark] = true;
        tmTrack("VideoProgress", { video: name, percent: mark });
      }
    });
  });

  video.addEventListener("ended", function () {
    if (sent.done) return;
    sent.done = true;
    tmTrack("VideoCompleted", { video: name });
  });
});

For a YouTube, Vimeo or Wistia embed, the idea is the same: listen to the player's own play and progress events (from its JavaScript API) and call tmTrack from them.

Example: button clicks

Tracks every click on elements marked with data-tm-click, so you can tag buttons in your HTML without writing more JavaScript:

<a href="/demo" data-tm-click="DemoButtonClicked" data-tm-location="hero">Book a demo</a>
document.addEventListener("click", function (e) {
  var el = e.target.closest("[data-tm-click]");
  if (!el) return;
  tmTrack(el.dataset.tmClick, { location: el.dataset.tmLocation || "" });
});

Example: a section comes into view

Fires once when a visitor scrolls your pricing table into view:

var pricing = document.querySelector("#pricing");
if (pricing && "IntersectionObserver" in window) {
  var observer = new IntersectionObserver(function (entries) {
    if (entries[0].isIntersecting) {
      tmTrack("PricingViewed");
      observer.disconnect();
    }
  }, { threshold: 0.5 });
  observer.observe(pricing);
}

Example: quiz or calculator steps

Send the step and the answer as details, so you can see where people drop off:

tmTrack("QuizStep", { step: 2, answer: "Under $10k per month" });
tmTrack("QuizCompleted", { result: "growth-plan" });

If the quiz ends with an email opt-in, fire a standard Lead at that point too, so your ad platforms count it as a lead.

No code? Use the Events page

For simple rules (a URL match, a click on a button's text, scroll depth, time on page) you do not need code at all. Create the event on the Events page in your dashboard instead. Use tmFireEvent when the action is something only your code knows about, like video progress or a quiz answer.

Framework notes

Same API, different placement per framework. Pick yours.

Plain HTML / Webflow / static site

Snippet goes in the page <head> (or your site builder's site-wide "head" / "custom code" / "tracking" field).

<!DOCTYPE html>
<html>
  <head>
    <script async src="https://YOUR-CNAME/t/s-custom.js"></script>
  </head>
  <body>
    <!-- ... -->

    <!-- On your thank-you page only: -->
    <script>
      window.tmOrder = {
        order_id: "<?php echo $order['id']; ?>",
        email:    "<?php echo $order['email']; ?>",
        currency: "USD",
        items:    [{ sku: "subscription", price: 7700, qty: 1 }]
      };
    </script>
  </body>
</html>

Substitute the templating syntax for whatever your stack uses (Jinja, Liquid, ERB, EJS, Handlebars).

React (Next.js)

Inject the snippet through next/script in your root layout. Call the API from useEffect or event handlers.

// app/layout.tsx
import Script from "next/script";

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <Script
          src="https://YOUR-CNAME/t/s-custom.js"
          strategy="afterInteractive"
        />
        {children}
      </body>
    </html>
  );
}
// app/thank-you/page.tsx
"use client";
import { useEffect } from "react";

export default function ThankYouPage({ order }: { order: Order }) {
  useEffect(() => {
    (window as any).tmOrder = {
      order_id: order.id,
      email:    order.customerEmail,
      currency: order.currency,
      items:    order.items.map(i => ({
        sku:   i.sku,
        name:  i.name,
        price: Math.round(i.priceUnits * 100),
        qty:   i.quantity,
      })),
    };
  }, [order]);

  return <h1>Thanks for your order!</h1>;
}
// In your opt-in form success handler
async function onSubmit(values: FormValues) {
  await api.subscribe(values);
  (window as any).tmFireEvent?.("Lead", { source: "newsletter" }, {
    email:      values.email,
    first_name: values.firstName,
    last_name:  values.lastName,
  });
}

TypeScript types. Add this to a .d.ts file in your project so calls type-check:

// types/truemetriks.d.ts
declare global {
  interface Window {
    tmFireEvent?: (
      name: string,
      customData?: Record<string, unknown> | null,
      userData?: Record<string, unknown> | null,
    ) => void;
    tmIdentify?: (email: string, traits?: Record<string, unknown>) => void;
    tmOrder?: {
      order_id: string;
      email?: string;
      currency?: string;
      items: Array<{
        sku?: string;
        name?: string;
        price: number;
        qty?: number;
        category?: string;
        brand?: string;
        variant?: string;
        image_url?: string;
      }>;
    };
  }
}
export {};

React (Vite / Create React App)

No next/script available - put the snippet straight into index.html so the browser parses it before your React bundle hydrates:

<!-- index.html -->
<head>
  <!-- ... -->
  <script async src="https://YOUR-CNAME/t/s-custom.js"></script>
</head>

Everything else (firing events in handlers, setting window.tmOrder in useEffect) is identical to the Next.js example above.

Vue (Nuxt 3)

// nuxt.config.ts
export default defineNuxtConfig({
  app: {
    head: {
      script: [
        { src: "https://YOUR-CNAME/t/s-custom.js", async: true }
      ]
    }
  }
});
<!-- pages/thank-you.vue -->
<script setup lang="ts">
const order = useOrder();

onMounted(() => {
  window.tmOrder = {
    order_id: order.value.id,
    email:    order.value.customerEmail,
    currency: order.value.currency,
    items:    order.value.items.map(i => ({
      sku: i.sku, name: i.name, price: Math.round(i.priceUnits * 100), qty: i.quantity
    })),
  };
});
</script>

Astro / Svelte / Solid / other

If your framework lets you put a <script> tag in the document head at build time, that is the right place for the install snippet. Then call window.tmFireEvent / window.tmIdentify / set window.tmOrder from your component lifecycle hooks.

Verifying the install

After the snippet is on every page and your code is calling the API:

  1. Pageview. Open any page on your site. DevTools > Network tab > filter for your CNAME hostname. You should see s-custom.js load (200) and one or more t/x or t/p requests (200).
  2. InitiateCheckout. Open your checkout page or step. The Network tab should show a POST /t/x with event_name: "InitiateCheckout".
  3. Lead. Submit your opt-in form (or trigger your form handler). The Network tab should show a POST /t/x with event_name: "Lead". The TrueMetriks dashboard's Events screen should list it within ~10 seconds.
  4. Purchase. Place a test order. On the thank-you page, check the browser console: window.tmOrder should show your populated object. Network tab should show POST /t/x with event_name: "Purchase" returning 200.
  5. Refresh the thank-you page. No second Purchase should fire (sessionStorage dedup on order_id).

See Test your integration for the full walkthrough including ad-platform verification (Facebook Events Manager, Google Ads Tag Diagnostics, etc).

Something not firing? See Troubleshooting.

Frequently asked questions

Does my framework matter?

No. The API is the same in plain HTML, React (Next.js / Vite / CRA), Vue, Svelte, Astro, Solid - anywhere you can run JavaScript in a browser. The only thing that changes is where you place the snippet in your codebase and where you call the API functions. See the framework notes below for the common patterns.

Why is React a special case?

It is not - the tracker API is identical for React. The reason React feels different is that React apps render via JavaScript, so dropping a raw <script> tag into JSX does not work. You inject the snippet through next/script, react-helmet, or your index.html, and you call window.tmOrder / window.tmFireEvent inside useEffect or event handlers - same API, just React-idiomatic placement.

When does Pageview fire?

Automatically on every page load and every SPA route change (the tracker hooks history.pushState and popstate). You never call it manually.

When does InitiateCheckout fire?

When your code fires it. On a custom install nothing fires InitiateCheckout by itself, so call window.tmFireEvent("InitiateCheckout", { value, currency }) once when your checkout page or step loads, with value in DOLLARS. There is no Checkout URL pattern to set for a custom install; that field only exists for GoHighLevel and ClickFunnels.

How do I fire Purchase?

Set window.tmOrder on your thank-you page with order_id, email, currency, and an items array (each item carrying sku, name, price in cents, qty). The tracker polls every 500ms for up to 30 seconds, fires Purchase + Identify the moment the object is populated, and remembers the order_id in sessionStorage so refreshes do not double-count.

How are same-page upsells handled?

Mutate window.tmOrder.order_id and window.tmOrder.items after each upsell. The tracker keeps polling and dedups by order_id, so every new order fires once and re-fires of the same order are absorbed.

Are prices in dollars or cents?

It depends which method you use. In window.tmOrder, each items[].price is in CENTS - a $49.00 product is price: 4900. When you fire a purchase yourself with window.tmFireEvent("Purchase", { value }), value is in DOLLARS - $49.00 is value: 49.00. Match the unit to the method you are using.

How do I track signups or registrations?

Call window.tmFireEvent("CompleteRegistration", { content_name, status, value, currency }, { email, first_name, last_name, phone }) from your signup success handler - every field in the middle object is optional. There is nothing to switch on: the event is forwarded to your connected pixels from the first fire, and it also appears, already ticked, as a new checkbox under Settings > Integrations > Facebook > Allowed events so you can switch it off if you ever want to.

Do I need to call tmIdentify separately, or is the user_data on tmFireEvent enough?

For most events, just passing user_data on tmFireEvent is enough - the tracker stitches identity per-event for ad-platform fanout. Call tmIdentify(email, traits) once when you first know the visitor's email if you want the identity persisted in sessionStorage and applied to every subsequent event in the same session (recommended on opt-in success and login success).

Can I track things other than leads, bookings and purchases?

Yes. Call window.tmFireEvent("AnyNameYouLike", { ...details }) from your own code and TrueMetriks records it as a custom event: a video watched to 50%, a pricing tab opened, a calculator used, a demo button clicked, anything your JavaScript can detect. It shows up in your dashboard (Conversions > All events dropdown > Custom events) and is sent to Facebook and TikTok as a custom event under the same name. See the Custom events section below for examples.

Does it work without cookies?

Pageview and event tracking still fire, but match quality to ad platforms drops. The tracker writes a first-party session cookie on your CNAME domain - blocking that cookie removes the visitor-stitching across page loads.

Need help with this?

Reach us instantly on WhatsApp, or send us a message and we will get back to you. On workdays we usually respond in less than 24 hours.

or send us a message