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.tmOrderon your thank-you page, or firetmFireEvent("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
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
<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.).
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 namecustomData(object, optional) - event-specific context. ForPurchasethis carriesvalue,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.tmOrderon 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
Purchasefrom your code the moment each charge succeeds, withwindow.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
);
valueis in dollars here (47.00) - unlikewindow.tmOrder.items[].price, which is in cents. This is the one place the two purchase methods differ.order_idmust be unique per charge. Use your payment processor's transaction id (Stripepi_..., 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:
- 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.
- 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.
- 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.
- Sends it to GA4 in snake_case (
PricingViewedbecomespricing_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
VideoProgressorDemoButtonClicked. No spaces. - Do not reuse a standard name (
Lead,Purchaseand 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:
- Pageview. Open any page on your site. DevTools > Network tab > filter for your CNAME hostname. You should see
s-custom.jsload (200) and one or moret/xort/prequests (200). - InitiateCheckout. Open your checkout page or step. The Network tab should show a
POST /t/xwithevent_name: "InitiateCheckout". - Lead. Submit your opt-in form (or trigger your form handler). The Network tab should show a
POST /t/xwithevent_name: "Lead". The TrueMetriks dashboard's Events screen should list it within ~10 seconds. - Purchase. Place a test order. On the thank-you page, check the browser console:
window.tmOrdershould show your populated object. Network tab should showPOST /t/xwithevent_name: "Purchase"returning 200. - 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.