Skip to main content

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.

Why this matters more than it used to

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://cdn.tntgrowth.io/flex-pixel.js"></script>
<script>
flexInit();
</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 register each HTTPS hostname to your account. Flex supplies the configured platform IDs after lookup. Existing explicit-ID snippets are still supported; they enable only the platforms explicitly listed.

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.

Call flexInit before sending events, including in a tag manager. A flexTrack call during inferred configuration lookup waits for readiness; a call before initialization starts is dropped. See flexInit never runs.

How platform selection works​

With flexInit(), your registered HTTPS hostname identifies the client, and Flex returns its public platform configuration. Missing configuration or ambiguous account selection prevents inferred initialization rather than guessing. Configure the correct accounts in Flex before installing.

A legacy snippet such as flexInit({ metaPixelId: 'YOUR_META_PIXEL_ID' }) still selects only that platform. Omitting Google Ads in a legacy snippet does not silently enable it after this upgrade. See installation options.

What you get for free​

With the snippet above in place and nothing else configured:

Click ids and UTMsRead from the URL on every pageview and stored in the attribution session
tnt_idMinted, written to a cookie and sessionStorage, and published as <meta name="tnt_id">
Your formsA hidden tnt_id input is appended to the forms you mark — see below
ConversionsEvery event the pixel sends carries the tnt_id with it
Enhanced ConversionsCaptured email and phone are passed to your Google tag
Mark the forms that feed your CRM

The pixel does not touch a form you have not marked. Add data-tnt-track to the form, or to any element around it:

<form method="post" data-tnt-track>…</form>

class="tnt-track" works identically, for page builders that let you set a class but not a data attribute. A wrapper works too, for builders that own the <form> element itself:

<div data-tnt-track="true">
<form method="post">…</form>
</div>

You still do not add the hidden field yourself — the pixel creates it. You are only telling it which forms are yours. Earlier versions injected every form on the page, which put tnt_id into site search and newsletter widgets that had no business carrying it.

One exception: running cookies: false, every form is injected whether or not you marked it, because with no cookie the hidden field is one of only two things carrying tnt_id to your CRM — an unmarked form would simply lose the attribution. Add formInjection: { optIn: true } to go back to marked forms only, once you have marked the ones that matter.

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. Mark your forms and create a tnt_id field in your CRM. The pixel puts the handle in the forms you marked with data-tnt-track; your CRM has to keep it. What to create, and what we need from you once you have:

CRMCreate a property namedWhat we need afterwards
HubSpottnt_idNothing — we read that name with no configuration
SalesforceTNT_ID__cThe 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 needs less setup than it looks

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 browser installation needs a registered HTTPS hostname belonging to an active client. The browser's request origin establishes that hostname; an arbitrary hostname in a payload does not.

The optional public pixelKey resolves the same client for configuration lookup and must agree with the registered origin. Invalid or conflicting keys fail without falling back. It grants no event or reporting access. Integration API keys and shared keys do not belong in browser snippets.

Explicit legacy IDs select platforms but never override ownership checks. They are not permission to track for another client. Unknown hosts, inactive clients, and conflicting account ownership must be corrected before requests can be accepted.

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://cdn.tntgrowth.io/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://cdn.tntgrowth.io/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.

Events wait for platform readiness. A missing initialization call or a failed inferred lookup cannot produce successful tracking; inspect both initialization diagnostics and the configuration request.

Fix: check window.flexInitialized in the console. false means initialization has not completed — check consent, the configuration response, hostname registration, account configuration, and script errors.

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 pixel honors flexConsent(false) and the host-managed flextrk_consent=0 cookie. Wire your consent platform to that API. Where prior permission is required, load the pixel after consent or call flexConsent(false) before initialization. Check these installation cases:

  • 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 declined but tracking is permitted, ask your contact about cookieless mode. It disables persistent storage while carrying attribution through approved forms and links; it never overrides a refusal to track.

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.

  1. window.flexInitialized is true. If not, check initialization, consent, and the configuration request.
  2. A request to /api/attribution/sessions appears 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.
  3. That request returned 200, not 403, 409 or 401. Read the message on a failure; each one names its cause and they are listed below.
  4. document.querySelector('meta[name="tnt_id"]').content returns a UUID. This is the handle for the visit, and its presence is the proof that the whole chain worked.
  5. Your form has a tnt_id input. Inspect it, then submit and confirm the value arrives in your CRM on the record.
  6. 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​

CodeMeaningWho fixes it
200Session opened, or an existing one matched—
204Deliberately not recorded, e.g. a crawler—
401The request carried no API key headerRe-copy the snippet
403Your site could not be identified, or the host is not one of your registered domainsRegister your hostnames with us
409A platform ID is registered to two accountsUs — tell your contact
429Rate limitedUsually 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:

  1. The full URL of a page where it fails, query string included.
  2. Your flexInit call, copied from the live page source.
  3. A screenshot of the Network tab filtered to attribution, showing the status code and the response body.
  4. Whether window.flexInitialized is true.

With those, the cause is usually one of the sections above.