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.
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
| Option | Type | Notes |
|---|---|---|
metaPixelId | string | Meta |
googleAdsCustomerId | string | Google Ads |
bingCustomerId | string | Microsoft Advertising |
linkedInAccountId | string | |
tiktokPixelId | string | TikTok |
redditPixelId | string | |
twitterPixelId | string | X (Twitter) |
googleAnalyticsMeasurementId | string | GA4 |
apiKey | string | Your own Flex API key, if we have issued you one. See Your own API key |
cookieDomain | string | Overrides the automatic cookie domain. See Sites spanning several domains |
googleEnhancedConversions | boolean | Defaults 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.
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.
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:
| Field | Notes |
|---|---|
platform | Required. Which platform to report to |
eventName | The event, e.g. Purchase, Lead |
eventTime | Epoch milliseconds. Defaults to now |
eventId | Your own id for the event, useful for deduplication |
conversionValue | Monetary value |
userData | Customer identifiers, e.g. email and phone |
customData | Any additional attributes you want recorded |
conversionActionId / conversionName / conversionId | Platform-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 option | Notes |
|---|---|
fieldName | The input's name attribute |
validationType | email, phone or text |
customValidator | Optional function replacing the built-in validation |
storeInCookie | Keep the value for use on later pages, e.g. a thank-you page |
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.
Navigating from your own JavaScript
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.
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 parameters — utm_source, utm_medium, utm_campaign, utm_term, utm_content,
utm_adgroup, utm_device, utm_matchtype
Ad click identifiers — gclid, 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=/.
| Cookie | Expires | Contents |
|---|---|---|
tnt_id | 100 days | Handle 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_matchtype | 90 days | The parameter's value |
One per click identifier — gclid, gbraid, wbraid, fbclid, msclkid, twclid, li_fat_id, ttclid, rdt_cid | 30 days | The 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, lastReferrer | 90 days | First and most recent referring domain |
firstTouchUrl, firstTouchUrlFull, lastTouchUrl, lastTouchUrlFull | 90 days | Entry and most recent page URLs |
firstTouchTime, lastTouchTime | 90 days | When those visits happened |
email, phone, text | 90 days | Only when a watched form field has storeInCookie: true |
_fbp | 90 days | Meta's browser identifier, written only if not already present |
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.
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.
Consent and privacy
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.
flexConsent, not flexInit, to change thisCalling 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
- Load a page with a campaign parameter, e.g.
?utm_source=test&gclid=test123. - In the browser console,
window.flexInitializedshould betrue. If it isfalse,flexInithas not run — check for a script error or a tag that did not fire. - Submit a watched form, or call
flexTrack, and confirm a request to the TNT Growth API appears in the Network tab. - Check that request's response status. A request appearing in the Network tab only shows an
attempt — a
4xxor5xxmeans it was rejected and nothing was recorded. Only a success response means TNT Growth accepted and queued the event. - 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
| Symptom | Most likely cause |
|---|---|
| No requests at all, no console errors | flexInit never ran. Events wait silently for it |
| Events for one platform missing | That platform's ID was not passed to flexInit |
| Form never captures | formIdentifier does not match the form's id or name |
| Attribution lost mid-funnel | The funnel crosses to a different domain — use flexUrlParamSync |
| Google reports no user-provided data | Enhanced 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.