Script tag
The script-tag build works on any site that lets you edit the <head>: static HTML, CMSs, page
builders, server-rendered apps.
<script>window.x3=window.x3||function(){(window.x3q=window.x3q||[]).push(arguments)}</script>
<script async src="https://api.3x.example/v1/s/x3.full.js?k=pk_3x_YOUR_PUBLISHABLE_KEY" data-key="pk_3x_YOUR_PUBLISHABLE_KEY"></script>With the first-party proxy the same snippet points at your own domain – the proxy path maps 1:1 onto the API root:
<script>window.x3=window.x3||function(){(window.x3q=window.x3q||[]).push(arguments)}</script>
<script async src="/n3/v1/s/x3.full.js?k=pk_3x_YOUR_PUBLISHABLE_KEY" data-key="pk_3x_YOUR_PUBLISHABLE_KEY" data-host="/n3"></script>Builds
| File | Contents | Size budget (gzip) |
|---|---|---|
x3.js | Core: pageviews (incl. SPA navigation), sessions, attribution, scroll, engagement, clicks (CTA, WhatsApp, messengers, downloads, outbound), custom events, data-x3-event, consent state, queue, transport, plugin loader | ≤ 14.5 KB |
x3.full.js | Core + every plugin below | ≤ 19.5 KB |
x3.video.js, x3.sections.js, x3.forms.js | Plugins, loaded on demand when the project config enables them or data-plugins lists them | ≤ 2.5 KB each |
x3.identify.js | identify() normalization and hashing (calls before it loads are queued) | ≤ 2.5 KB |
x3.consent.js | CMP adapters (Cookiebot, Usercentrics, Complianz, Borlabs, Google Consent Mode) | ≤ 2.5 KB |
x3.bookings.js | Booking adapters (HubSpot meetings, Calendly, thank-you pages, window events) | ≤ 2.5 KB |
The core is larger than a pageview-only counter because it also carries consent modes, the
conversion beacon, the npm/presets bridge and client-side canonicalPage rules – features that
must act before the first send. npm run size -w @3x-engine/sdk enforces these budgets in CI.
Versioned copies (x3@1.2.3.js) are served for pinning; unversioned files are cached for an hour
(5 minutes with ?k=) and revalidated in the background.
?k=pk_… inlines the project's client configuration (window.__x3cfg), so the tracker starts
without fetching GET /v1/c/{pk}. Without it the SDK fetches the config and holds sends until it
arrives (at most 5 s, then defaults are used).
Attributes
| Attribute | Default | Meaning |
|---|---|---|
data-key | – (required) | Publishable key pk_3x_… |
data-host | Origin of the script's src | Where batches and config go: the API origin or your proxy path (/n3) |
data-plugins | from the project config | Comma list: video,sections,forms |
data-consent | read from the consent key / CMP | pending or granted |
data-auto | all | Comma list of autocapture features to keep: pageviews,clicks,scroll,engagement,forms,video,sections |
data-hash-routing | off | Treat #/route changes as pageviews |
data-debug | off | Expose window.__x3 and log diagnostics |
A later x3("init", {...}) call overrides attributes (SDK reference).
The queue
The first line defines window.x3 as a queue until the script has loaded. Everything you call
before is replayed in order:
<script>
x3("track", "newsletter_signup", { list: "weekly" });
x3("consent", "granted");
</script>Queue commands: init, track (alias event), goal, funnelStep, page, identify,
consent, flush. After loading, window.x3 is the full API object as well as a function, so
x3.track("…") works too.
Single-page apps
The core patches history.pushState/replaceState and listens to popstate, so client-side
routing (Next.js, React Router, Vue Router, Astro view transitions, …) produces pageviews without
extra code. Hash routers: data-hash-routing. Routers without the History API: call
x3("page", "/path") yourself.
Loading the script twice (hot reload, duplicated tags) is harmless: a singleton guard keeps one tracker.
Content Security Policy
With the proxy: script-src 'self' and connect-src 'self' plus the hash of the inline stub. Without
the proxy: add the API origin to both. Details: CSP.