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 register your HTTPS hostnames. Platform accounts are configured in Flex; the default installation does not need their IDs in your page.

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. Flex uses the registered HTTPS hostname to load your configured platforms. Register each hostname, including landing-page hosts, before installing.

<script>
flexInit();
</script>

You can still pass options without platform IDs, for example flexInit({ cookies: false }). Initialization waits for the configuration lookup; window.flexInitialized becomes true only after trackers are ready. An unknown hostname, inactive client, missing platform configuration, or ambiguous GA4 measurement selection must be corrected in Flex before this mode can initialize.

Existing explicit-ID installations​

Existing snippets keep working unchanged:

flexInit({ metaPixelId: 'YOUR_META_PIXEL_ID' });

Supplying any platform-ID property selects the legacy method. Only the IDs you supply are enabled; omitted platforms are never added from your Flex account. This method can initialize without remote platform lookup, though requests still require the existing ownership checks. Do not add empty ID properties to a hostname-based installation. There is no removal date for explicit-ID support.

Configuration reference​

OptionTypeNotes
metaPixelIdstringMeta
googleAdsCustomerIdstringGoogle Ads
bingCustomerIdstringMicrosoft Advertising
linkedInAccountIdstringLinkedIn
tiktokPixelIdstringTikTok
redditPixelIdstringReddit
twitterPixelIdstringX (Twitter)
googleAnalyticsMeasurementIdstringGA4
pixelKeystringOptional public configuration-only key. See Public pixel configuration key
cookieDomainstringOpt in to shared attribution cookies on an owned parent domain; defaults to host-only. See Sites spanning several domains
googleEnhancedConversionsbooleanDefaults to true. See Enhanced Conversions
withHubSpotbooleanDefaults to false. See HubSpot

Platform IDs are optional in inferred mode and configured centrally in Flex. Providing any ID property selects the explicit legacy method instead.

HubSpot​

Off by default. Set withHubSpot: true only if the site actually runs HubSpot's tracking code:

flexInit({ metaPixelId: '...', withHubSpot: true });

With it on, identifiers the pixel has already captured — the Meta _fbp browser cookie, and any email, phone or name captured from your forms — are handed to HubSpot's tracker via _hsq.identify. They land on the HubSpot contact, which is what lets a conversion reported later from HubSpot carry them through to Meta.

Captured email, phone and names are withheld when tracking consent has been denied.

Leave it off without HubSpot

The pixel creates HubSpot's _hsq queue when HubSpot's own snippet has not loaded yet, so that the identify is processed before HubSpot's first page view — the only ordering that sends it. On a site with no HubSpot, that would leave captured identifiers queued on the page for something that never arrives.

Attribution reaches HubSpot through hidden form fields as well, which needs no configuration — see Populating a hidden field from stored attribution.

Public pixel configuration key​

Hostname lookup needs no key. If TNT Growth issues you a public pixel configuration key, you may pass it as pixelKey without supplying platform IDs:

flexInit({ pixelKey: 'YOUR_PUBLIC_PIXEL_CONFIG_KEY' });

This key must have the pixel-config scope and access only /api/attribution/pixel-config. It returns public configuration; it cannot authorize conversions, read reporting data, or change settings. Your HTTPS hostname must still be registered to the same active client. Invalid, revoked, shared, incorrectly scoped, or mismatched keys stop lookup without an anonymous fallback.

Do not combine pixelKey with explicit platform IDs: mixed input is rejected before tracking starts. Do not use an integration API key, shared server key, or platform access token. The old apiKey option must be removed: explicit-ID legacy snippets ignore it and never send it, while ID-free initialization rejects it. No shared API key is bundled with the pixel. The dashboard's default snippet deliberately needs no key.

Configuration lookup is bounded to two seconds. Events arriving during lookup wait and send once when initialization succeeds. Failure drops waiting events with a diagnostic; correct the cause and retry initialization. Consent denial drops events immediately, and revocation or a superseding initialization cancels events waiting on the old lookup. Repeated inferred initialization after success is ignored. Repeated explicit-ID calls preserve legacy behavior: they can add missing trackers and apply options, without replacing or duplicating existing trackers.

Identify the installed build​

Read window.flexPixelVersion in the browser console. Deployed builds identify themselves with a date and short commit, and send that value on configuration and conversion requests in X-Flex-Pixel-Version. Your TNT Growth contact can use it with the site hostname to find matching server logs. Older or unstamped bundles report no version.

Google Tag Manager​

Use a tag that loads the bundle and then calls flexInit() on all pages. The planned Gallery template will use hostname inference by default and offer explicit IDs as a legacy option; it is not shipped with this release. Only the public pixelKey above is suitable for its optional key field. Wire consent before initialization when prior permission is required.

Initialise before sending events

Call flexInit before flexTrack, including in a tag manager. Events arriving during inferred configuration lookup wait for readiness; events sent before initialization starts are dropped.

Call flexInit even in hostname mode. Events cannot be sent until platform initialization succeeds. A failed configuration lookup must be corrected before retrying; it does not report successful initialization.

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 match a platform resolved from Flex or selected in your explicit-ID snippet.

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​

Cookies are host-only by default. For a cookie-enabled funnel across your own trusted subdomains, set the same explicit parent domain on every participating host, retaining your existing options:

flexInit({ cookieDomain: 'example.com' });

This shares click IDs, UTMs, tnt_id, browser identity, A/B parameters and sanitized touchpoint data between www.example.com, lp.example.com and app.example.com. It also supports a later direct visit to the app without a decorated link, while those cookies remain available. Register every hostname in Flex as usual; this option does not change API origin authorization.

Only choose a domain whose subdomains you control and trust. The value must be the current hostname or its parent, and the browser must accept it as a cookie domain. Foreign domains, malformed values and browser-rejected public/private suffixes fall back to host-only behavior. cookies: false and consent refusal still prevent storage. Contact data, custom form cookies, _fbp and the visit cookie remain host-only.

On the first eligible visit after opting in, existing host attribution is promoted when no shared value exists; an existing shared value wins over a stale host copy. Migration gives promoted values their normal cookie lifetime because browsers do not expose the old expiry to JavaScript. On the apex host, existing host-only and shared cookies cannot be distinguished, so the first migration refreshes their lifetime once; a marker prevents repeating that renewal. On child hosts, existing shared first-touch/click cookies keep their expiry. Use the same configuration everywhere: a host running the default or an older build can remove the shared copy on a later write. A consent manager must apply withdrawal on each host; one host cannot delete another host's old host-only cookies or session storage. Previously deleted attribution cannot be reconstructed by this setting.

Cookies cannot span unrelated domains. If shared cookies are unsuitable, configure approved HTTPS link destinations explicitly and include click IDs and UTMs:

flexInit({
linkDecoration: {
domains: ['www.example.com', 'lp.example.com', 'app.example.com'],
utms: true,
},
});

With cookies enabled, linkDecoration: true alone carries only tnt_id; utms: true adds click IDs and UTMs. Decoration does not carry the original first-touch URL cookie. A visitor must follow a decorated link before the destination can persist the attribution for subsequent direct returns. It cannot bridge a landing visit followed by a first direct visit to another host with no handoff. includeSubdomains: true covers the current hostname and its children, not sibling hosts.

When link decoration is enabled, the pixel adds the attribution handle to approved links at click time, including links added by a framework or rendered late.

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.

Navigations across cookie boundaries

Use flexDecorate when the destination cannot read the source cookie. This includes cookieless journeys and journeys across hosts without shared cookies; the destination must be approved in linkDecoration.domains.

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.

Returning visitors and ad clicks​

A new click ID replaces the previous value for that platform. Google click identifiers (gclid, gbraid, wbraid) are selected from the same arrival, so an older GCLID cannot displace a newer braid. Eligible form fields also use recorded Google touch evidence on direct returns, clearing stale sibling identifiers when that evidence is available. A Meta arrival retains the earlier Google evidence. Direct visits and internal navigation without a new click ID do not clear it. First-touch URL/time remain acquisition history.

For example, Google A → Google B → direct visit → lead uses B for a new Google conversion. An event that happened before B still uses A when the browser has that history. Always supply the original eventTime when retrying or reporting an older event; a later timestamp changes which clicks preceded it. Explicitly supplied click IDs take precedence over inferred browser history.

The pixel retains up to 100 observed touches in consent-controlled local storage for its existing 30-day click-retention period, pruning expired entries on subsequent captures. A separate cookie per platform shares the latest observed touch when cookieDomain is configured. Older history remains origin-local; clearing storage or moving between unrelated domains can leave it unavailable. These are retention limits, not a guarantee of platform attribution eligibility. Legacy cookies without observation history keep their compatibility fallback; their actual click time is unknown.

Campaign fields describe the selected touch only when that pairing is known. Do not combine a Google click ID with Meta campaign tags merely because both are currently stored. The reporting models (first-touch, last-touch, linear and time-decay) distribute analytical credit separately; changing a report model does not change conversion identity or resend an already delivered conversion.

Cookies the pixel sets​

Pixel cookies use path=/, Secure and explicit SameSite=Lax, and are written only over HTTPS when storage and tracking are allowed. They are host-only by default; cookieDomain explicitly shares the attribution cookies described above.

CookieExpiresContents
tnt_id100 daysHandle for the attribution session this visit created. Not for your own use — see the warning below
_flextrk_cookie_scope100 daysHost-only migration marker recording the accepted shared domain after cookieDomain is accepted; prevents repeated migration on the apex host
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
_flextrk_touch_*30 daysInternal latest-touch snapshot per platform; do not modify or use as a conversion ID
originalReferrer, lastReferrer90 daysFirst and most recent referring domain
firstTouchUrl, lastTouchUrl90 daysSanitized entry 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

Public capture cookies are named after what they hold — a captured email field writes email, and a Google click ID writes gclid. Internal _flextrk_* cookies and storage keys are reserved.

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.

Contact data remains local to the host

Stored contact fields such as email and phone remain host-only even with cookieDomain enabled. Touchpoint URLs are sanitized; retired firstTouchUrlFull and lastTouchUrlFull cookies are cleared. Do not place secrets or personal information in URLs or enable cookie storage for sensitive fields.

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

The pixel honors flexConsent(false) and a host-managed flextrk_consent=0 cookie. Wire your consent platform to flexConsent; without an explicit refusal, tracking is allowed by default. Where prior consent is required, load the pixel only after permission or deny before calling flexInit.

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 control tracking permission explicitly. Call flexConsent(false) before initialization, then grant permission through your banner. cookies: false independently disables persistent pixel storage; granting tracking permission does not turn storage on. Once tracking is permitted, cookieless attribution uses a server-side session and approved forms and links:

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

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

In this mode the hidden tnt_id field goes into every form, not only the ones you marked — with no cookie it is one of only two things carrying the handle to your CRM. Pass formInjection: { optIn: true } alongside cookies: false to keep the marked-forms-only rule, and formInjection: { deny: ['.site-search'] } to exclude a single form either way.

If changing the snippet is awkward — it lives in your repository and ships on your release — ask us instead. The same settings can be configured on our side for your domain, and your pixel picks them up on its next load with no deploy of yours.

flexConsent(true) permits tracking unless the host consent cookie still vetoes it. Storage remains disabled when cookies: false; otherwise permitted attribution can be persisted. flexConsent(false) stops tracking, cancels pending requests, and clears pixel-owned storage.

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 completed — check the configuration request, hostname registration, configured accounts, consent state, and any script error.
  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 eventsCheck flexInit, consent, and the configuration request result
Events for one platform missingAccount missing in Flex, or omitted from the explicit legacy snippet
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.