Skip to main content

Pixel Installation

The TNT Growth pixel is a small JavaScript bundle for your website. It reads campaign attribution from the URL, keeps it as visitors move through your site, and reports conversion events back to TNT Growth so they can be attributed to the ad click that produced them.

Your TNT Growth contact will give you the bundle URL and the platform IDs for your account — ask them for the ready-made snippet, which comes with your IDs already filled in.

Copy a working installation instead of assembling one

If you would rather see a complete correct installation and the mistakes that quietly lose attribution, start with Good and Bad Installations. It covers the one that costs the most — a pixel installed only on the thank-you page.

1. Add the script

Load the bundle in <head>, before any script that calls it:

<script src="https://YOUR-PIXEL-HOST/flex-pixel.js"></script>

Your TNT Growth contact will give you the exact host to use.

You can also install it through a tag manager. If you do, load it on all pages — attribution is captured on the landing page and used later on your conversion page, so a pixel that only loads on the conversion page will have nothing to attribute.

2. Initialise

Call flexInit once per page load with the IDs for the platforms you advertise on. Include only the platforms you use.

<script>
flexInit({
metaPixelId: 'YOUR_META_PIXEL_ID',
googleAdsCustomerId: 'YOUR_GOOGLE_ADS_CUSTOMER_ID',
});
</script>

Configuration reference

OptionTypeNotes
metaPixelIdstringMeta
googleAdsCustomerIdstringGoogle Ads
bingCustomerIdstringMicrosoft Advertising
linkedInAccountIdstringLinkedIn
tiktokPixelIdstringTikTok
redditPixelIdstringReddit
twitterPixelIdstringX (Twitter)
googleAnalyticsMeasurementIdstringGA4
apiKeystringYour own Flex API key, if we have issued you one. See Your own API key
cookieDomainstringOverrides the automatic cookie domain. See Sites spanning several domains
googleEnhancedConversionsbooleanDefaults to true. See Enhanced Conversions

At least one platform ID is required — with none, there is nothing to report to.

Your own API key

If we have issued you an API key, pass it as apiKey:

<script>
flexInit({
metaPixelId: 'YOUR_META_PIXEL_ID',
apiKey: 'YOUR_FLEX_API_KEY',
});
</script>

It is optional. Without it the pixel uses a shared key and we identify you by your hostname or your platform IDs instead, which is how every installation worked before this option existed — so nothing breaks by leaving it out.

Passing it is more accurate: two advertisers can share a platform ID, and a hostname only counts once you have told us about it, whereas a key belongs to exactly one account.

This key is not a secret, and does not need to be

It sits in your page where anyone can read it, like your platform IDs. It is scoped to the specific endpoints the pixel uses and grants nothing else — it cannot read your data or change your settings. Ask us for a new one if you ever want it rotated.

Order does not have to be perfect

If a flexTrack call runs before flexInit, it waits and sends once initialisation completes. You do not need to guarantee script order in a tag manager.

The reverse is not true: if flexInit never runs at all, events wait indefinitely and nothing is sent.

3. Track an event

flexTrack({
platform: 'google',
eventName: 'Purchase',
eventTime: Date.now(),
conversionValue: 100.0,
});

platform is required and selects where the event goes: meta, google, bing, linkedin, tiktok, reddit, twitter or googleAnalytics. It must be a platform you passed to flexInit.

Commonly used fields:

FieldNotes
platformRequired. Which platform to report to
eventNameThe event, e.g. Purchase, Lead
eventTimeEpoch milliseconds. Defaults to now
eventIdYour own id for the event, useful for deduplication
conversionValueMonetary value
userDataCustomer identifiers, e.g. email and phone
customDataAny additional attributes you want recorded
conversionActionId / conversionName / conversionIdPlatform-specific conversion identifiers

Send one flexTrack call per platform you want the event reported to.

Automatic form capture

Rather than calling flexTrack yourself on submit, you can have the pixel watch a form and capture fields as they are submitted.

watchForm({
formIdentifier: 'contact-form',
fields: [
{ fieldName: 'email', validationType: 'email', storeInCookie: true },
{ fieldName: 'phone', validationType: 'phone' },
{ fieldName: 'company', validationType: 'text' },
],
});

formIdentifier matches the form's id or its name attribute.

Field optionNotes
fieldNameThe input's name attribute
validationTypeemail, phone or text
customValidatorOptional function replacing the built-in validation
storeInCookieKeep the value for use on later pages, e.g. a thank-you page
Forms that load late

You can call watchForm immediately even if the form is added to the page afterwards — by a framework, an embedded widget, or a popup. The pixel watches for it and attaches when it appears.

Calling watchForm again for the same formIdentifier replaces the previous watcher rather than adding a second one, so it is safe to call on route changes in a single-page app.

Populating a hidden field from stored attribution

If your form needs to submit attribution to your own system, flexFieldWatch fills an input from a stored value when it comes into view:

flexFieldWatch({
fieldName: 'gclid', // <input name="gclid" type="hidden">
cookieName: 'gclid',
});

It returns a function that stops watching, which is worth keeping in a single-page app:

const stopWatching = flexFieldWatch({ fieldName: 'gclid', cookieName: 'gclid' });
// later, on unmount
stopWatching();

Tuning goes in an options property of the same argument object, not a second argument:

flexFieldWatch({
fieldName: 'gclid',
cookieName: 'gclid',
options: { maxAttempts: 40, interval: 250 },
});

options accepts maxAttempts (default 20), interval in ms (default 300), and onError / onSuccess callbacks. The field is looked for up to maxAttempts times before it gives up.

Sites spanning several domains

Attribution is stored in cookies at your registrable domain, so it survives movement between subdomains — a landing page on www.example.com to a booking page on app.example.com works with no extra configuration.

Cookies cannot be shared across genuinely different domains. If your funnel sends visitors to a third-party domain, pass the attribution in the URL instead:

flexUrlParamSync(['/book-a-call', '/schedule']);

On those paths, campaign parameters held in cookies are re-appended to the URL, so the destination page receives them as ordinary query parameters.

The pixel adds the attribution handle to a link at the moment it is clicked, so links added by a framework, rendered late, or living inside a web component are all covered without configuration.

What it cannot see is a navigation your own code performs, because no click ever happens:

window.location.href = '/thank-you'; // the pixel is never told about this

Wrap the URL in flexDecorate when you do that:

window.location.href = flexDecorate('/thank-you');

It returns the URL with tnt_id added, and returns it unchanged if there is no handle yet or the destination is not one of your own domains — so it is always safe to wrap, and it can never hand your attribution to a third-party site.

Only matters without cookies

With cookies on, the handle is in a cookie and survives any navigation, so this is optional. Under cookies: false the URL is the only carrier, and an unwrapped location.href loses the visit.

Enhanced Conversions

Google Enhanced Conversions improve match rates by sending hashed customer data alongside the click. This is on by default: the pixel passes captured email and phone to your Google tag, both when a form is submitted and on page load if a value was stored earlier. This is usually what clears Google's "tag is firing but not capturing user-provided data" diagnostic.

To disable it for the whole site:

flexInit({ googleAdsCustomerId: '...', googleEnhancedConversions: false });

To disable it for one form only:

watchForm({ formIdentifier: 'internal-form', googleEnhancedConversions: false, fields: [...] });

What the pixel reads from the URL

Captured automatically on every page load and kept for the visit:

Campaign parametersutm_source, utm_medium, utm_campaign, utm_term, utm_content, utm_adgroup, utm_device, utm_matchtype

Ad click identifiersgclid, gbraid, wbraid (Google), fbclid (Meta), msclkid (Microsoft), twclid (X), li_fat_id (LinkedIn), ttclid (TikTok), rdt_cid (Reddit)

You do not need to do anything for these to be picked up.

Cookies the pixel sets

All cookies are first-party, set on your own registrable domain with a leading dot (so they work across all your subdomains) and path=/.

CookieExpiresContents
tnt_id100 daysHandle for the attribution session this visit created. Not for your own use — see the warning below
One per campaign parameter — utm_source, utm_medium, utm_campaign, utm_term, utm_content, utm_adgroup, utm_device, utm_matchtype90 daysThe parameter's value
One per click identifier — gclid, gbraid, wbraid, fbclid, msclkid, twclid, li_fat_id, ttclid, rdt_cid30 daysThe identifier's value. The shortest lifetime here, so a click id can expire while the campaign parameters that arrived with it are still present
originalReferrer, lastReferrer90 daysFirst and most recent referring domain
firstTouchUrl, firstTouchUrlFull, lastTouchUrl, lastTouchUrlFull90 daysEntry and most recent page URLs
firstTouchTime, lastTouchTime90 daysWhen those visits happened
email, phone, text90 daysOnly when a watched form field has storeInCookie: true
_fbp90 daysMeta's browser identifier, written only if not already present
Check these names against your own cookies

Cookies are named after what they hold, with no prefix — a captured email field writes a cookie called email, and a Google click ID writes gclid. Only tnt_id is namespaced.

tnt_id is a bearer token for one visit's attribution. Read it if you are debugging, but do not copy it into a URL or a form field of your own — the pixel already carries it where it needs to go, and a copy landing in a page URL can be misread downstream as a Google click ID.

If your site already uses any of these names, the two will overwrite each other. Tell your TNT Growth contact before installing and a different approach can be arranged.

First-touch values are preserved. originalReferrer, firstTouchUrl and firstTouchTime are written only if not already set, so the original entry point survives for the full 90 days. The last* values update on every page load.

Two of these can hold personal data

firstTouchUrlFull and lastTouchUrlFull store the complete URL including its query string, and email / phone store what a visitor typed. All of them are readable by every subdomain of your site for 90 days.

Two things follow. Keep secrets and personal data out of query strings — a token or an email address passed as a URL parameter ends up in a cookie that lives for three months. And leave storeInCookie off for sensitive form fields unless you trust every subdomain that can read it, since only the fields you explicitly mark are persisted.

Need to read these values in your own code? See Reading Cookie Values.

The pixel sets these cookies as soon as it loads, and it does not read your consent platform itself. You have two ways to keep that under your control.

Load the pixel after consent. Through your consent platform's tag rules, or by deferring the script. Simplest, and right when you would rather the pixel not run at all before a visitor decides. The cost is that a visitor who accepts partway through a visit has no attribution from before they accepted.

Or start cookieless and upgrade. Pass cookies: false and the pixel stores nothing — no cookies, no localStorage, no sessionStorage — while still attributing the visit: the session is minted server-side and carried on decorated links instead. When your banner reports consent, call flexConsent(true):

flexInit({ googleAdsCustomerId: '123-456-7890', cookies: false });

window.addEventListener('cookieConsentUpdate', (event) => {
flexConsent(event.detail.accepted);
});

flexConsent(true) turns storage on and writes what this pageview already holds — the attribution handle and the campaign parameters — so the visit continues in the same session rather than starting a second one. flexConsent(false) clears those cookies and stops storing again.

Call flexConsent, not flexInit, to change this

Calling flexInit a second time with cookies: true appears to work and does not. Storage turns on, but attribution, form injection and link decoration have all already initialised and will not run again, and nothing writes the handle the page is holding — so the next pageview finds an empty jar and starts a new session.

Checking it works

  1. Load a page with a campaign parameter, e.g. ?utm_source=test&gclid=test123.
  2. In the browser console, window.flexInitialized should be true. If it is false, flexInit has not run — check for a script error or a tag that did not fire.
  3. Submit a watched form, or call flexTrack, and confirm a request to the TNT Growth API appears in the Network tab.
  4. Check that request's response status. A request appearing in the Network tab only shows an attempt — a 4xx or 5xx means it was rejected and nothing was recorded. Only a success response means TNT Growth accepted and queued the event.
  5. Once you have a success response, ask your TNT Growth contact to confirm the conversion reached the ad platform. That last step is visible only on our side: acceptance and platform delivery are separate, and a queued event can still fail downstream.

Troubleshooting

SymptomMost likely cause
No requests at all, no console errorsflexInit never ran. Events wait silently for it
Events for one platform missingThat platform's ID was not passed to flexInit
Form never capturesformIdentifier does not match the form's id or name
Attribution lost mid-funnelThe funnel crosses to a different domain — use flexUrlParamSync
Google reports no user-provided dataEnhanced Conversions disabled, or no email/phone field captured

If you are still stuck, send your TNT Growth contact the page URL, your flexInit call, and a screenshot of the Network tab — that is almost always enough to identify it.

For each of these worked through in full, with the installation that causes it and the fix, see Good and Bad Installations.