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.
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
| 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 |
pixelKey | string | Optional public configuration-only key. See Public pixel configuration key |
cookieDomain | string | Opt in to shared attribution cookies on an owned parent domain; defaults to host-only. See Sites spanning several domains |
googleEnhancedConversions | boolean | Defaults to true. See Enhanced Conversions |
withHubSpot | boolean | Defaults 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.
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.
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:
| 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
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.
Navigating from your own JavaScript
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.
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.
| Cookie | Expires | Contents |
|---|---|---|
tnt_id | 100 days | Handle for the attribution session this visit created. Not for your own use — see the warning below |
_flextrk_cookie_scope | 100 days | Host-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_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 |
_flextrk_touch_* | 30 days | Internal latest-touch snapshot per platform; do not modify or use as a conversion ID |
originalReferrer, lastReferrer | 90 days | First and most recent referring domain |
firstTouchUrl, lastTouchUrl | 90 days | Sanitized 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 |
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.
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.
Consent and privacy
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.
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 completed — check the configuration request, hostname registration, configured accounts, consent state, and any script error. - 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 events | Check flexInit, consent, and the configuration request result |
| Events for one platform missing | Account missing in Flex, or omitted from the explicit legacy snippet |
| 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.