Custom install
TrueMetriks tracks your pageviews and checkouts automatically and lets you fire 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 the URL contains your configured pattern (automatic)
- Lead when you call
tmFireEvent("Lead", ...)(manual - from your form handlers) - Schedule when you call
tmFireEvent("Schedule", ...)(manual - from your booking confirmations) - Purchase when you set
window.tmOrderon your thank-you page, or firetmFireEvent("Purchase", ...)per charge in an upsell funnel - Any custom event name you call
tmFireEvent("YourEventName", ...)with
Lead, Schedule, and Purchase are forwarded to Facebook, Google Ads, TikTok, and GA4.
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", "Identify". Any other string is treated as a custom event.
- customData: optional object with event-specific custom payload.
For Purchase: { value, currency, order_id, content_ids, num_items, items }.
For Lead/Schedule/custom: optional context, e.g. { source: "newsletter" }.
- 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
- InitiateCheckout when the URL contains the configured Checkout URL pattern
(default: "checkout")
- First-party session cookie on the CNAME domain
- utm_*, gclid, fbclid, ttclid, wbraid, gbraid capture on first touch
WHAT I WANT YOU TO DO:
- Ask me: (a) what framework my site is built on (plain HTML, Next.js, Vite,
CRA, Vue/Nuxt, Astro, etc.), (b) what events I want to fire and from where
(which form, which route, which button), (c) what my Checkout URL pattern
should be, (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.
- 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. Set the Checkout URL pattern (optional)
Same install card has a Checkout URL pattern field. The tracker watches the page URL and fires InitiateCheckout whenever the URL contains the word you configure. Default is checkout. Change it to whatever word appears in your checkout-step slug (order, cart, payment, etc.).
The match is case-insensitive and substring-based. Setting it to cart matches /cart-page, /your-cart, /cart-funnel-3.
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.
- InitiateCheckout when the page URL contains your configured Checkout URL pattern.
- 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: leads, schedules, and purchases.
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, Purchase, Identify. Any other name is treated as a custom event and forwarded as-is to the ad platforms that accept custom events.
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.
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 | Automatic (URL pattern) |
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 |
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) |
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"
});
Custom events
Any string is a valid event name. Use this for product-specific events your funnel cares about:
window.tmFireEvent("WatchedVSL", { length_seconds: 180 });
window.tmFireEvent("AddedToWishlist", { sku: "SKU-1" });
window.tmFireEvent("DownloadedWhitepaper", { paper_id: "roas-2026" });
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. Visit a URL that contains your Checkout URL pattern. 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?
Automatically when the current URL contains the word configured as your Checkout URL pattern (default checkout). Change the value in the install card if your checkout step uses a different slug.
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.
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).
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.