Good and Bad Installations
Most attribution loss we investigate is not a bug in the pixel. It is an installation that runs, logs no errors, and fires events — while quietly failing to record the ad click that produced them.
This page shows one correct installation in full, then the ways an installation goes wrong. Each bad example is one we have actually had to diagnose.
The pixel no longer keeps attribution in the browser alone. When a visitor arrives from an ad, it asks
TNT Growth to open an attribution session and hands the page one opaque id — tnt_id — which
stands in for the click ids and campaign parameters stored on our side.
That session is opened once, on the pageview the visitor arrives on. Everything downstream — the conversion you send us days later, the CRM record your sales team closes next month — is joined back to the ad click through it. An installation that misses the arrival pageview has nothing to join to, and no later call can repair it.
A good installation
Two script tags in <head>, on every page of the site.
<!DOCTYPE html>
<html>
<head>
<!-- Start TNT Growth Pixel -->
<script src="https://YOUR-PIXEL-HOST/flex-pixel.js"></script>
<script>
flexInit({
metaPixelId: '1234567890123456',
googleAdsCustomerId: '123-456-7890',
});
</script>
<!-- End TNT Growth Pixel -->
<!-- the rest of your head -->
</head>
<body>
...
</body>
</html>
Your TNT Growth contact will give you the pixel host, and can send you this exact snippet with your account's IDs already filled in — ask for it rather than typing the IDs by hand, because a mistyped ID is one of the failure modes below.
That is the whole required installation. Everything below is why each part of it is the way it is.
Why it is in <head>, not before </body>
The pixel reads the click id out of the URL and opens the attribution session as soon as it runs. In
<head> that happens before the page has finished rendering, so a visitor who lands and immediately
clicks through to another page is still recorded.
At the bottom of <body> it races the visitor. On a slow connection a bounce can beat the script.
Why it is on every page, not just the conversion page
This is the single most common installation mistake, and it has its own section below.
Why flexInit is inline and immediate
flexInit is what creates the platform trackers, and no event is sent until it has run. Keeping it in
a plain inline <script> directly after the bundle means it cannot be deferred, reordered or dropped
by a bundler.
Order is otherwise forgiving: a flexTrack call that runs before flexInit waits and sends once
initialisation completes, so you do not need to guarantee script order across a tag manager. The
reverse is not forgiving — see flexInit never runs.
Why the platform IDs matter beyond your own tags
The IDs you pass do two jobs. They fire your own Meta and Google tags client-side, and they are one of the ways we identify which client an attribution session belongs to. An install with no IDs and no registered domain cannot be identified at all — the session is refused. See How we identify your site.
What you get for free
With the snippet above in place and nothing else configured:
| Click ids and UTMs | Read from the URL on every pageview and stored in the attribution session |
tnt_id | Minted, written to a cookie and sessionStorage, and published as <meta name="tnt_id"> |
| Your forms | A hidden tnt_id input is appended automatically, so the handle reaches your CRM |
| Conversions | Every event the pixel sends carries the tnt_id with it |
| Enhanced Conversions | Captured email and phone are passed to your Google tag |
Form injection is on by default. You do not add the hidden field yourself.
The two things worth doing on your side
1. Register your hostnames with us. Tell your TNT Growth contact every hostname the pixel will run
on — example.com, www.example.com, book.example.com, any landing-page host. A request arriving
from a hostname we have on file identifies your account server-side, which is the strongest
identification a browser can offer.
2. Create a tnt_id field in your CRM. The pixel puts the handle in your form; your CRM has to
keep it. What to create, and what we need from you once you have:
| CRM | Create a property named | What we need afterwards |
|---|---|---|
| HubSpot | tnt_id | Nothing — we read that name with no configuration |
| Salesforce | TNT_ID__c | The field's API name, sent to your TNT Growth contact |
Salesforce is the exception because it returns only the fields we ask for by name. Every handle field
has to be named in your mapping before we read it — the standard TNT_ID__c included. On HubSpot the
standard names work untouched.
Map your form's tnt_id field to it. Without this the handle reaches your form and is thrown away on
submit, and every conversion you send us arrives unattributed.
Using a field you already have, under a name of your own? That works on either CRM — send your contact the exact API name and we will read that one too.
HubSpot already records the page URL a submission came from in hs_analytics_last_url and
hs_analytics_first_url, and we read those too. So a HubSpot client gets partial coverage with no
property created at all. Create the property anyway — the URL path only works where the handle reached
the URL, which is not the default.
How we identify your site
Every attribution session is filed against one client. Three things can establish which, and they are not equally good evidence:
| How | Strength | What it needs |
|---|---|---|
| A client-scoped API key | Strongest — a credential only your install holds | Issued by us on request |
| Your hostname | Nearly as strong — a browser cannot forge the host it is on | Your hostnames registered with us |
A platform ID from your flexInit | Weakest — these IDs are readable in your page source | Nothing; it is the fallback |
If none of the three resolves, the session is refused with 403 and
"Could not identify a client for this request." Nothing is recorded.
The practical consequence: register your hostnames. It costs you one email, it upgrades every session you record, and it is also what protects you — once we hold your domains, a request arriving from anywhere else claiming your platform IDs is rejected rather than written into your data.
Bad installations
The pixel is only on the thank-you page
By far the most common, and the most expensive.
<!-- On /thank-you only. Nowhere else. -->
<script src="https://YOUR-PIXEL-HOST/flex-pixel.js"></script>
<script>
flexInit({ googleAdsCustomerId: '123-456-7890' });
flexTrack({ platform: 'google', eventName: 'Lead' });
</script>
What you see: the event fires. The Network tab shows a request. It returns success. Everything looks fine.
What actually happened: by the time the visitor reached /thank-you, the ?gclid=… from the ad
click was many pages behind them. No pixel ran on the page that had it, so no attribution session was
ever opened. The conversion is real and it is recorded — with nothing identifying the click, the
campaign or the keyword that produced it. In your ad platform it is invisible.
Fix: load the pixel on every page. If you install through a tag manager, set the trigger to All Pages, not to your conversion page.
Query parameters are stripped before the pixel runs
Visitor clicks the ad → https://example.com/lp?gclid=Cj0KCQ…&utm_source=google
Your edge redirect fires → https://example.com/lp ← parameters gone
Pixel runs → nothing to attribute
A canonical-URL redirect, a marketing router, a CMS that rewrites to a clean URL, or an A/B testing tool that reloads without the query string will all do this. The pixel is installed perfectly and still records nothing, because the redirect ran first.
What you see: attribution sessions exist for some traffic and not others, with no obvious pattern. Often paid traffic is missing while UTM-tagged email traffic is fine, because only one of the two survives your redirect rules.
Fix: preserve the query string across redirects. If you cannot, load the pixel on the page that still has the parameters, before the redirect.
flexInit never runs
<script src="https://YOUR-PIXEL-HOST/flex-pixel.js"></script>
<script>
// flexInit is in a tag that never fires, or a bundle that failed, or was simply forgotten
flexTrack({ platform: 'meta', eventName: 'Lead' });
</script>
What you see: nothing. No requests, no console errors, no complaints.
flexTrack is deliberately patient — it retries on a timer until initialisation completes, so that tag
managers can fire tags in any order. The cost of that tolerance is that a pixel which is never
initialised waits forever instead of failing loudly.
Fix: check window.flexInitialized in the console. false means flexInit has not run — look for
a tag that did not fire or a script error earlier on the page.
Someone else's snippet was copied
<script>
// Copied from another site's page source, or from an old client's install
flexInit({ metaPixelId: 'SOMEONE_ELSES_PIXEL_ID' });
</script>
Platform IDs are visible in page source, so this is easy to do by accident — usually when a template or a landing-page builder is reused across accounts.
What you see: 403 on the attribution request, with "Request origin is not one of the registered
domains for the identified client." Once we hold the real owner's domains, their IDs cannot be used
from your host.
There is a second version of this. If the same platform ID is genuinely registered to two active
accounts on our side, it identifies neither, and the request is refused with 409 and "The supplied
platform identifier is registered to more than one active client." That one is ours to fix — tell your
contact and we will separate the accounts.
Fix: ask your TNT Growth contact for the snippet generated for your account, and replace the whole block rather than editing the IDs in place.
The form serialiser ignores the injected field
// A hand-rolled submit handler that names the fields it sends
async function onSubmit(event) {
event.preventDefault();
await fetch('/api/lead', {
method: 'POST',
body: JSON.stringify({
email: form.email.value,
phone: form.phone.value,
// tnt_id is in the DOM. It is not in this object, so it is not sent.
}),
});
}
The pixel appends <input type="hidden" name="tnt_id"> to your form, which covers an ordinary browser
submit and any handler built from new FormData(form). It cannot help a handler that lists its fields
explicitly.
What you see: sessions are minted correctly, the hidden input is present if you inspect the form, and every conversion still arrives without a handle.
Fix: read the field, or read the meta tag the pixel publishes:
const tntId = document.querySelector('meta[name="tnt_id"]')?.content ?? null;
Either works. The meta tag is the more reliable of the two for a framework form, because it does not depend on the input surviving a re-render.
The consent banner blocks the pixel indefinitely
The pixel does not check for consent — it acts as soon as it loads, which is why it should load after consent is granted. Two failure modes sit either side of that:
- Loaded before consent, and your consent platform blocks it entirely for visitors who never interact with the banner. Those visitors are never attributed.
- Loaded after consent, but the consent gate also blocks it for visitors who accepted, because the tag's trigger was never updated.
Fix: gate the pixel on your consent platform's granted trigger, and confirm with a real accepted
session that window.flexInitialized is true afterwards. If you operate in a market where marketing
cookies are frequently declined, ask your contact about cookieless mode — the attribution session is
still opened server-side, and the handle travels through the form instead of a cookie.
A single-page app that only initialises once, on a route that is not the entry point
// Runs in the router, on navigation — not on first paint
router.afterEach(() => {
flexInit({ googleAdsCustomerId: '123-456-7890' });
});
The visitor's entry pageview is the one carrying the click id. A pixel initialised on the first navigation has already missed it.
Fix: put the snippet in the HTML shell that every route is served from, so it runs on first paint. The pixel handles route changes on its own; you do not need to re-initialise it.
Verifying an installation
Run this on a page loaded with a test click id — ?gclid=TNT_TEST_123&utm_source=test. A TNT_TEST_
prefix is recognised as a QA visit, so it produces a real session without being uploaded to Google.
window.flexInitializedistrue. If not,flexInitdid not run.- A request to
/api/attribution/sessionsappears in the Network tab. If it is absent, the URL carried neither a click id nor a UTM — the pixel does not ask us about traffic with nothing to attribute. - That request returned
200, not403,409or401. Read the message on a failure; each one names its cause and they are listed below. document.querySelector('meta[name="tnt_id"]').contentreturns a UUID. This is the handle for the visit, and its presence is the proof that the whole chain worked.- Your form has a
tnt_idinput. Inspect it, then submit and confirm the value arrives in your CRM on the record. - Navigate to another page and check the meta tag again. A different UUID means the handle is not surviving navigation; the same one means it is.
A 204 on step 2 is not a failure. It is us saying the pageview was not worth a session — a known
crawler, most often.
Response codes on the attribution request
| Code | Meaning | Who fixes it |
|---|---|---|
200 | Session opened, or an existing one matched | — |
204 | Deliberately not recorded, e.g. a crawler | — |
401 | The request carried no API key header | Re-copy the snippet |
403 | Your site could not be identified, or the host is not one of your registered domains | Register your hostnames with us |
409 | A platform ID is registered to two accounts | Us — tell your contact |
429 | Rate limited | Usually a loop re-initialising the pixel |
What to send us when it is still wrong
Attribution problems are almost always diagnosable from four things, and we can rarely do anything without them:
- The full URL of a page where it fails, query string included.
- Your
flexInitcall, copied from the live page source. - A screenshot of the Network tab filtered to
attribution, showing the status code and the response body. - Whether
window.flexInitializedistrue.
With those, the cause is usually one of the sections above.