Meta - Adding a Meta pixel without Tag Manager

Adding a Meta pixel without Tag Manager


Every project has a Footer scripts field, which injects raw script tags at the end of the public page. That is enough to run a complete Meta pixel — base code, story events, scoring, consent — with no Tag Manager container at all.


This article covers when that is the right call, and gives you a snippet that only needs four lines changed per client.


💡 In one sentence: paste one script into Footer scripts, change the pixel ID, and you get the same five events Tag Manager would send — at the cost of debuggability and client self-service.


Which route to choose

Consideration

Tag Manager

Footer script

Time to first event

Slower — the client must provision container access

Minutes, if you have admin on the project

Who can change it later

The client's own marketers

Only an Intractive admin

Debugging

Tag Assistant shows every tag and why it did or didn't fire

Browser console only

Several destinations

Add a tag

Edit code

Versioning and rollback

Built in

None


Use Tag Manager when the client wants to own their tracking, when Analytics is in scope too, or when the campaign will run long enough for someone to want changes.


Use the footer script when the client has no container and no appetite to get one, when the campaign is short and the events are already agreed, or when you need something live this week.


⚠️ Never run both at once. If a project has Meta tags in Tag Manager and this script, every event is sent twice to the same pixel. There is no event_id on either, so Meta counts both as real. This was measured: a single story start produced two ViewContent events. Pick one route and remove the other.


The five events

Every Intractive story sends the same five. Keeping the vocabulary fixed matters — all our stories share one domain, and Meta budgets configurable events per domain, not per client. Inventing new event names per project burns a shared allowance and makes accounts inconsistent.

Event

Meta type

Fires on

What it's for

ViewContent

standard

Story start

Audience seed, and the denominator for every drop-off number

StoryHalfway

custom

Reached 50%

Engagement. Tells you whether the ad or the story is the problem

StoryComplete

custom

Reached 100%

Retargeting people who finished but didn't click

Lead

standard

Outbound link click

The conversion. Optimise campaigns on this

QualifiedCandidate

custom

Score reached, or segment assigned

Mid-funnel optimisation once volume allows

ViewContent and Lead are Meta standard events, selectable as campaign objectives with no extra setup. The other three are custom and need a Custom Conversion in Events Manager before you can optimise toward them.





⚠️ Lead means "clicked through to the destination", not "applied". The actual application happens in the client's ATS, out of reach of a pixel. Be precise about this when reporting, or you will promise a client something the number does not support.


Two ways to profile people

Both are optional. Configure neither and you get four clean events.


Scoring — for "how interested is this person?". The story adds points as people answer; when the total crosses a threshold, QualifiedCandidate fires and the score travels as value, which is what makes value-based lookalikes possible later.
Segmentation — for "which role fits this person?". The story writes a category into a variable; that category rides along as a segment parameter on every event after it is known.


💡 Send categories as a parameter, never as event names. Resist FitEngineering, FitWarehouse, and so on. One event carrying segment: "engineering" gives you unlimited segments inside the fixed schema — build one Custom Conversion per segment in Events Manager and each becomes its own audience.



⚠️ Keep segment values to neutral role labels. engineering, warehouse, customer-support. Never age, nationality, parental status, salary expectation, language or accessibility needs. A segment is inferred personal data about an individual, sent to an advertising platform, in a recruitment context — encoding anything that proxies a protected characteristic is a problem both under Meta's terms and under employment law.


Installing it

Project settings → Tracking & scripts → Footer scripts. Admin only.
Paste the code below, wrapped in a pair of script tags, and change the config block at the top. Do not also paste the base pixel code Meta offers you in Events Manager — the bootstrap is already included here, and pasting both initialises the pixel twice.
You can skip Meta's noscript image fallback too. It exists for visitors with JavaScript disabled, and a story is a JavaScript app — those visitors see nothing to track.


💡 Only the four lines inside CONFIG change per client. Everything below it is identical for every story we ever ship.




<script>(function () {

/* CONFIG - edit this block only */

var CONFIG = {
pixelId: '000000000000000', // client's pixel ID
currency: 'EUR',
score: { variable: 'intent_score', threshold: 60 }, // or variable: null
segment: { variable: null }, // or e.g. 'best_fit'
trackHalfway: true,
requireConsent: false
};

/* END CONFIG - nothing below needs editing */

var ctx = { story_id: null, session_id: null },
fired = {}, started = false, score = null,
segment = null, enabled = true,
consent = !CONFIG.requireConsent;

function readVar(vars, name) {
if (!vars || !name) return undefined;
return vars['@' + name]; // story variables carry an @ prefix
}

function send(key, name, type, extra) {
if (fired[key] || !enabled || !consent) return;
if (typeof fbq !== 'function') return;
fired[key] = true;
var p = { story_id: ctx.story_id, session_id: ctx.session_id };
if (segment) p.segment = segment;
if (extra) for (var k in extra) p[k] = extra[k];
fbq(type, name, p);
}

function qualify() {
var byScore = CONFIG.score && CONFIG.score.variable &&
score !== null && score >= CONFIG.score.threshold,
bySeg = CONFIG.segment && CONFIG.segment.variable && segment;
if (!byScore && !bySeg) return;
send('qualified', 'QualifiedCandidate', 'trackCustom',
score === null ? null : { value: score, currency: CONFIG.currency });
}

function handle(o) {
if (!o || !o.event) return;
if (o.event === 'disableTracking') { enabled = false; return; }
if (o.event === 'enableTracking') { enabled = true; return; }

if (o.storyId) ctx.story_id = o.storyId;
if (o.sessionId) ctx.session_id = o.sessionId;

if (o.event === 'intractiveVariablesUpdated') {
var s = Number(readVar(o.variables, CONFIG.score && CONFIG.score.variable));
if (!isNaN(s)) score = s;
var g = readVar(o.variables, CONFIG.segment && CONFIG.segment.variable);
if (g) segment = String(g);
if (started) qualify();
return;
}

if (o.event === 'intractiveStoryProgress') {
var pr = Number(o.progress);
// A genuine run always opens at progress 0. A resumed session jumps
// straight to its saved position - ignore it, or every reload books
// another completion.
if (pr === 0) { started = true; send('view', 'ViewContent', 'track'); return; }
if (!started) return;
if (CONFIG.trackHalfway && pr >= 50) send('half', 'StoryHalfway', 'trackCustom');
if (pr >= 100) send('done', 'StoryComplete', 'trackCustom');
return;
}

if (o.event === 'intractiveOutboundLink' && started) {
send('lead', 'Lead', 'track', {
link_url: o.url,
value: score === null ? 0 : score,
currency: CONFIG.currency
});
}
}

if (!window.fbq) {
!function (f, b, e, v, n, t, s) {
if (f.fbq) return; n = f.fbq = function () {
n.callMethod ? n.callMethod.apply(n, arguments) : n.queue.push(arguments);
};
if (!f._fbq) f._fbq = n; n.push = n; n.loaded = !0; n.version = '2.0';
n.queue = []; t = b.createElement(e); t.async = !0; t.src = v;
s = b.getElementsByTagName(e)[0]; s.parentNode.insertBefore(t, s);
}(window, document, 'script', '<https://connect.facebook.net/en_US/fbevents.js>');
}

fbq('init', CONFIG.pixelId);
if (consent) fbq('track', 'PageView');

window.intractiveMetaConsent = function () {
if (consent) return;
consent = true;
fbq('track', 'PageView');
for (var i = 0; i < dl.length; i++) handle(dl[i]);
};

var dl = window.dataLayer = window.dataLayer || [];
for (var i = 0; i < dl.length; i++) handle(dl[i]);
var push = dl.push;
dl.push = function () {
var r = push.apply(dl, arguments);
for (var j = 0; j < arguments.length; j++) handle(arguments[j]);
return r;
};

})(); </script>

What the snippet already handles

  • Resumed sessions. Reloading a finished story sends only PageView. See Why completions get over-counted for why this matters.
  • Once per page. Each event fires at most once per load.
  • The story's own tracking toggle. If tracking is disabled in story settings, the script stops sending.
  • Late consent. With requireConsent: true nothing is sent until your consent tool calls window.intractiveMetaConsent(). At that point everything the visitor already earned is replayed rather than lost.


Verifying it

Open the story in a private window — not a story you have already finished — and run this in the browser console:

performance.getEntriesByType('resource')
.filter(e => e.name.includes('/tr'))
.map(e => new URL(e.name).searchParams.get('ev'))


On a fresh start you should see ["PageView", "ViewContent"], each exactly once. Anything appearing twice means the pixel is loaded twice — check whether Tag Manager is also running Meta tags on this project.


To confirm the right pixel is live:

Object.keys(window.fbq.instance.pixelsByID)


💡 This console check is the source of truth, not Events Manager. It tells you what the browser actually sent, with no reporting delay in between. Use it first, every time.


Reading Events Manager without being misled

Events Manager is slow, and its default view hides today. Three traps, all of which have cost us time:

Trap

What to do

The date range ends yesterday

The default range excludes today. A brand-new pixel has only today's data, so it always looks dead. Set the range to Today before concluding anything

New events take hours to register

Meta says 30 minutes. For an event name or a dataset it has never seen, it has taken several hours, then everything appeared at once. Verify on the wire and wait

The overview can contradict itself

We have seen the table report no activity while its own chart plotted dozens of events. When they disagree, trust neither — go back to the console check


⚠️ "Finish setting up Meta Pixel · 0% complete" is onboarding UI, not a diagnosis. It stays at 0% until Meta processes your first events. It is not telling you the pixel is broken.



✅ Quick checklist

  • One route only — Tag Manager or footer script, never both.
  • Change the pixel ID; leave everything below CONFIG alone.
  • Don't paste Meta's base code as well.
  • Use the fixed five-event schema; send categories as a segment parameter.
  • Neutral role labels only.
  • Test from a private window and check the console, not Events Manager.
  • Set the Events Manager date range to Today before judging anything.

Updated on: 24/09/2026

Was this article helpful?

Share your feedback

Cancel

Thank you!