Web Pixel
Track web visits, engagement, and conversions from your LinkForty links with a single <script> tag. The web pixel is the browser-side counterpart to the mobile SDKs — where the SDKs attribute app installs, the pixel attributes web sessions and on-site conversions back to the link that drove them.
The web pixel is a LinkForty Cloud feature. It sends events to the Cloud ingestion endpoint and shows up in the Web Analytics dashboard. Self-hosted Core does not include the pixel ingestion pipeline.
What it does
- Page views — every page load, including client-side route changes in single-page apps
- Engagement — time on page, scroll depth, and bounce detection
- Conversions — declarative (
data-lf-event) or programmatic (window.LinkForty.track(...)), with optional revenue values - Link attribution — the click id (
lf_click) and UTM parameters on the landing URL attribute the session (and its conversions) to the originating link - Cross-domain tracking (opt-in) — carry that attribution to a second site you send visitors to, such as a store on another domain
- Autocapture (opt-in) — outbound clicks, file downloads, and form submissions with no code
- Privacy-first — respects Do Not Track and Global Privacy Control, strips query strings and hashes from URLs, and never stores raw IP addresses
Using Claude Code, Cursor or Codex? Copy this prompt and paste it into your agent from the root of your site's project. It finds where every page gets its <head>, adds the tag, lists your hostnames for the allowed-domain list, and verifies a page view in Web analytics. It needs your site key; the prompt asks you for it.
Show the prompt
I use LinkForty (https://linkforty.com), a deep-linking and mobile attribution platform: short links that route each click to the app, the app store or the web, and record clicks, installs and in-app events per link. Before writing any code, read the LinkForty documentation index at https://docs.linkforty.com/llms-full.txt and the pages listed under "Docs" below. Work from what the docs say, not from assumptions about how similar products work. If something you need is not in the docs or in this prompt — an ID, a token, a URL — stop and ask me for it rather than inventing a placeholder that looks real. ## Task: install the LinkForty web pixel on this site Add the LinkForty pixel so that page views, engagement and conversions on this site are recorded and attributed to the LinkForty links that brought visitors here. The pixel is a single `<script>` tag carrying the site key; page views and engagement are automatic once it loads. ## Values - **Site key**: shown in the LinkForty dashboard under Workspace Settings → Web Pixel (it starts with `ws_`). Ask me for it. It is public and belongs in the page HTML; it only lets a page *send* events to my workspace. - **Not the app token.** The mobile SDK's app token (`at_…`) is a different credential for a different surface; the pixel does not use it. If you find one in this codebase, leave it alone. - **Allowed domains**: the workspace can restrict which sites may send events (Workspace Settings → Web Pixel → Allowed domains). Events from any other host are dropped silently, so list every hostname this site is served from — production, www and non-www, staging if it should count — and tell me to add any that are missing. - **Ingestion endpoint**: the default (`https://api.linkforty.com/api/pixel/v1/event`) is right for LinkForty Cloud; leave `data-endpoint` unset. ## How to work 1. **Look before you change anything.** Identify the framework, its version, the navigation/router library and where the app initializes at launch. List the screens (routes) you found, and **ask me which screen each incoming link should open and what parameter it needs** — for example "a link with `productId` opens the product screen". Wait for my answer before editing; do not guess routes. 2. **Work on a new git branch.** If the working tree has uncommitted changes, stop and tell me instead of building on top of them. 3. **Follow the docs pages named below, not memory.** Package names, method names and config keys come from those pages; LinkForty's SDKs are not the same as other attribution SDKs you may know. 4. **Do not invent values.** Anything in "Values" that says "ask me" is something you ask for. Never commit a token to source. 5. **Keep the app working without a link.** A launch with no matching click must behave exactly as before: no crash, no error surfaced to the user, no blocked start-up. 6. **Finish with a summary**: every file you changed and why; the manual steps that remain for me (Apple Team ID and bundle ID, Play signing SHA-256 fingerprint, store listing, a rebuild and reinstall) and where in the LinkForty dashboard each one goes (Workspace Settings → App Configuration); and anything you were unsure about. 7. **Verify.** Run the app once, then check the LinkForty dashboard: the SDK Doctor badge in the sidebar (https://docs.linkforty.com/guides/attribution/sdk-health) should report the SDK as installed. If the MCP server is available, create a test link with `create_link` and open it on a device or simulator; the click and the deep link should reach the screen we agreed on. ## Steps 1. **Find where every page gets its `<head>`.** Identify the framework and the one place a tag reaches every route: the root layout in a Next.js App Router project (`app/layout.tsx`, using `next/script` with `strategy="afterInteractive"`), `pages/_document.tsx` or `_app.tsx` in the Pages Router, `nuxt.config` `app.head` in Nuxt, the application layout in Rails, `index.html` in a Vite or plain-HTML site, the theme header or a header-scripts setting in WordPress. Tell me which you chose and why before editing. 2. **Add the tag** exactly as the docs show (https://docs.linkforty.com/sdks/web-pixel#install): `<script src="https://cdn.linkforty.com/pixel.js" data-workspace="…" async></script>` with `data-workspace` set to the site key from "Values", loaded `async`. Do not inline the pixel's source, wrap it in a consent gate I have not asked for, or add it to a tag manager instead of the page. 3. **Leave the defaults unless the site needs otherwise** (https://docs.linkforty.com/sdks/web-pixel#configuration-attributes): SPA route tracking is on by default and covers React, Vue, Next.js and SvelteKit client-side navigation (https://docs.linkforty.com/sdks/web-pixel#spa-route-tracking); autocapture and visitor IDs are off, and visitor IDs are consent-sensitive — do not enable them. 4. **List the hostnames** this site is served from (from the framework config, environment files, deployment config or CNAME records you can see) so I can check them against Allowed domains. Do not guess a domain you cannot find. 5. **Check the site's Content-Security-Policy**, if it has one: `cdn.linkforty.com` must be allowed as a script source and `api.linkforty.com` as a connect source, or the pixel loads but sends nothing. 6. **Verify**: run the site, load a page, and confirm in the browser's network tab that a request goes to the ingestion endpoint (a `POST …/api/pixel/v1/event` with `Content-Type: text/plain`). Then check the LinkForty dashboard: Analytics → Web should show the page view within a minute. If the site's own privacy settings honor Do Not Track, note that the pixel disables itself in a browser with DNT or Global Privacy Control on (https://docs.linkforty.com/sdks/web-pixel#privacy) — test in one without. ## Docs - Web pixel: https://docs.linkforty.com/sdks/web-pixel (Install, Configuration attributes, SPA route tracking, Privacy, Site key) - Web analytics, including how a visit is attributed to a link: https://docs.linkforty.com/guides/analytics/web-analytics#how-web-attribution-works - Troubleshooting: https://docs.linkforty.com/sdks/web-pixel#troubleshooting ## Done when - The tag is in the one place every page renders, with the site key, loaded async. - A page load produces a `page_view` request to the ingestion endpoint, and it appears under Analytics → Web in the LinkForty dashboard. - I have the list of hostnames to confirm under Allowed domains. When you are done, tell me: exactly which files changed and why, how I can verify it worked end to end (what to tap or run, and what I should see in the LinkForty dashboard), and anything you were unsure about.
Install
Copy the snippet from your dashboard under Workspace Settings → Web Pixel (it's pre-filled with your workspace's site key), or build it yourself:
<script src="https://cdn.linkforty.com/pixel.js" data-workspace="ws_your_site_key" async></script>
Paste it into the <head> of every page you want to track. That's the entire installation — page views and engagement are tracked automatically.
Use the WordPress plugin instead. It adds the pixel to every page and reports WooCommerce purchases as conversions, with no theme editing.
Your site key (ws_…) lives under Workspace Settings → Web Pixel. It is public and safe to ship in your HTML. See Site key for rotation.
Configuration attributes
All configuration is read from data-* attributes on the script tag:
| Attribute | Required | Default | Description |
|---|---|---|---|
data-workspace | Yes | — | Your public workspace site key (ws_<32 hex chars>). Routes events to your workspace. |
data-endpoint | No | https://api.linkforty.com/api/pixel/v1/event | Override the ingestion endpoint (e.g. when routing through a custom domain). |
data-spa | No | true | Single-page-app route tracking. Set "false" to disable and send only the initial page view. |
data-autocapture | No | false | Set "true" to auto-track outbound clicks, downloads, and form submits. |
data-visitors | No | false | Set "true" to assign a persistent visitor ID for new-vs-returning analytics. Consent-sensitive — see Visitor ID. |
data-link-domains | No | — | Hosts this site sends visitors to, comma-separated ("store.com, shop.example"). Links and forms pointing at them carry the session's attribution across. See Cross-domain tracking. |
Example with everything enabled:
<script src="https://cdn.linkforty.com/pixel.js"
data-workspace="ws_a1b2c3d4e5f6789abcdef0123456789a"
data-autocapture="true"
data-visitors="true"
async></script>
Tracking conversions
A conversion is any meaningful action you want to attribute back to a link — a signup, a purchase, a demo request. There are two ways to fire one.
Show the prompt
I use LinkForty (https://linkforty.com), a deep-linking and mobile attribution platform: short links that route each click to the app, the app store or the web, and record clicks, installs and in-app events per link.
Before writing any code, read the LinkForty documentation index at https://docs.linkforty.com/llms-full.txt and the pages listed under "Docs" below. Work from what the docs say, not from assumptions about how similar products work. If something you need is not in the docs or in this prompt — an ID, a token, a URL — stop and ask me for it rather than inventing a placeholder that looks real.
## Task: send conversion events from this site to LinkForty
The LinkForty pixel is already installed on this site (if it is not, stop and tell me — that is a separate task). Page views are flowing; what is missing is the conversions: the sign-ups, purchases and leads that make a visit worth something. Add them so each one is recorded against the link that brought the visitor.
## Values
- **Site key**: shown in the LinkForty dashboard under Workspace Settings → Web Pixel (it starts with `ws_`). Ask me for it. It is public and belongs in the page HTML; it only lets a page *send* events to my workspace.
- **Not the app token.** The mobile SDK's app token (`at_…`) is a different credential for a different surface; the pixel does not use it. If you find one in this codebase, leave it alone.
- **Allowed domains**: the workspace can restrict which sites may send events (Workspace Settings → Web Pixel → Allowed domains). Events from any other host are dropped silently, so list every hostname this site is served from — production, www and non-www, staging if it should count — and tell me to add any that are missing.
- **Ingestion endpoint**: the default (`https://api.linkforty.com/api/pixel/v1/event`) is right for LinkForty Cloud; leave `data-endpoint` unset.
## How to work
1. **Look before you change anything.** Identify the framework, its version, the navigation/router library and where the app initializes at launch. List the screens (routes) you found, and **ask me which screen each incoming link should open and what parameter it needs** — for example "a link with `productId` opens the product screen". Wait for my answer before editing; do not guess routes.
2. **Work on a new git branch.** If the working tree has uncommitted changes, stop and tell me instead of building on top of them.
3. **Follow the docs pages named below, not memory.** Package names, method names and config keys come from those pages; LinkForty's SDKs are not the same as other attribution SDKs you may know.
4. **Do not invent values.** Anything in "Values" that says "ask me" is something you ask for. Never commit a token to source.
5. **Keep the app working without a link.** A launch with no matching click must behave exactly as before: no crash, no error surfaced to the user, no blocked start-up.
6. **Finish with a summary**: every file you changed and why; the manual steps that remain for me (Apple Team ID and bundle ID, Play signing SHA-256 fingerprint, store listing, a rebuild and reinstall) and where in the LinkForty dashboard each one goes (Workspace Settings → App Configuration); and anything you were unsure about.
7. **Verify.** Run the app once, then check the LinkForty dashboard: the SDK Doctor badge in the sidebar (https://docs.linkforty.com/guides/attribution/sdk-health) should report the SDK as installed. If the MCP server is available, create a test link with `create_link` and open it on a device or simulator; the click and the deep link should reach the screen we agreed on.
## Steps
1. **Confirm the pixel is present** — find the `cdn.linkforty.com/pixel.js` tag and note where it loads. If the site has no pixel, stop here and tell me.
2. **Find the completion points.** Look for the moments a visitor has *finished* something valuable: account created, order confirmed, payment succeeded, demo or contact form submitted, trial started. List them with the file and the exact point in the flow, and **ask me which ones to track and what to call each** before writing anything. Good names are short, stable, lower_snake_case: `signup_completed`, `checkout_completed`, `demo_requested` (https://docs.linkforty.com/guides/analytics/events#event-naming-conventions).
3. **Fire each one at completion, never at render.** A conversion must fire once, when the thing has actually happened — on the success callback, the confirmation page, the server's OK — not when a page loads, a form mounts, or a button becomes visible. A conversion that fires on render inflates the numbers permanently and cannot be undone. If the success moment is only known server-side (payment webhooks), fire it on the confirmation page the user lands on afterwards, guarded so a reload does not fire it twice.
4. **Use the right mechanism** (https://docs.linkforty.com/sdks/web-pixel#tracking-conversions): a plain click that *is* the conversion can use the declarative attribute `data-lf-event="…"`; anything after an async step uses `window.LinkForty.track('event_name', { … })` (https://docs.linkforty.com/sdks/web-pixel#programmatic). Where the code may run before the pixel has loaded, push onto the pre-load queue instead (https://docs.linkforty.com/sdks/web-pixel#tracking-before-the-pixel-loads).
5. **Put money in `value`.** `value` is the amount LinkForty counts as revenue: for a purchase, pass `{ value: <number>, currency: '<ISO 4217 code>' }` (`data-lf-value` in the declarative form) so it rolls up under Goals & revenue (https://docs.linkforty.com/guides/analytics/web-analytics#goals--revenue). A property named `revenue` is not counted. Numbers only — no currency symbols, no strings. Only money actually received goes in `value`: a cart total when checkout starts goes under another name such as `cart_total`, or it is counted as revenue. Add a couple of useful flat properties (plan, product id); no names, emails or other personal data.
6. **Tell me the goal names.** Conversion goals are chosen in the dashboard under Analytics → Web → Conversion goals; only names in that list count as conversions once any are configured. List the event names you added so I can enter them.
7. **Verify** by completing one conversion on the running site and watching the network tab for the `conversion` event with the right name and value, then confirming it under Analytics → Web in the LinkForty dashboard.
## Docs
- Tracking conversions (declarative, programmatic, pre-load queue): https://docs.linkforty.com/sdks/web-pixel#tracking-conversions
- Goals & revenue: https://docs.linkforty.com/guides/analytics/web-analytics#goals--revenue
- Event naming: https://docs.linkforty.com/guides/analytics/events#event-naming-conventions
- How a conversion is attributed to a link: https://docs.linkforty.com/guides/analytics/web-analytics#how-web-attribution-works
## Done when
- Each agreed conversion fires exactly once, at completion, with revenue where money changed hands.
- Nothing fires on page load or on form render.
- The conversions appear under Analytics → Web, and I have the list of names to enter under Conversion goals.
When you are done, tell me: exactly which files changed and why, how I can verify it worked end to end (what to tap or run, and what I should see in the LinkForty dashboard), and anything you were unsure about.Declarative (no JavaScript)
Add data-lf-event to any clickable element. When the element (or anything inside it) is clicked, the pixel fires a conversion with that name. Add data-lf-value for a numeric value such as revenue.
<button data-lf-event="signup_completed">Sign up</button>
<button data-lf-event="checkout_completed" data-lf-value="49.00">
Complete purchase
</button>
| Attribute | Description |
|---|---|
data-lf-event | Conversion event name (max 255 chars). Fired on click. |
data-lf-value | Optional numeric value, counted as revenue. Non-numeric values are ignored. |
Programmatic
Call window.LinkForty.track() from your own code — for conversions that happen after an async step (payment confirmation, server response):
window.LinkForty.track('checkout_completed', { value: 49.0, currency: 'USD', plan: 'pro' });
window.LinkForty.track(eventName: string, properties?: Record<string, unknown>): void
eventName— the conversion name (max 255 chars).properties— optional flat object of custom data. Values may be primitives or arrays of primitives; nested objects are dropped. Limited to 30 keys, 512 characters per string.valueis the amount counted as revenue (with an optionalcurrencycode) in Web Analytics goals & revenue; a property namedrevenueis stored but not counted. Put any amount that isn't revenue, such as a cart total, under another name.
Tracking before the pixel loads
Because the script is async, it may not be ready when your code runs. Queue calls on window.LinkForty.q and the pixel replays them on load:
window.LinkForty = window.LinkForty || { q: [] };
window.LinkForty.q.push(['signup', { plan: 'pro' }]);
JavaScript API
The pixel exposes a small global on window.LinkForty:
| Member | Type | Description |
|---|---|---|
track(name, properties?) | function | Fire a conversion event imperatively. |
version | string | The loaded pixel version (e.g. "1.2.3"). |
q | array | Pre-load queue — push [name, properties] tuples here before the pixel loads. |
Event types
Every event the pixel sends is one of three types:
| Type | When it fires | Key data |
|---|---|---|
page_view | Initial load and each SPA route change (debounced, de-duplicated by URL) | page title, language, URL, referrer, UTM |
engagement | Once when the tab is hidden or the page unloads | time on page, scroll depth, bounce |
conversion | data-lf-event clicks, track() calls, and autocapture | event name, value, custom properties |
All events also carry the page URL (query string and hash removed), the referrer (likewise trimmed), parsed UTM parameters, and the session/visitor IDs when available.
Transport
Events are sent with navigator.sendBeacon() (falling back to fetch with keepalive) as a POST to the ingestion endpoint with Content-Type: text/plain. This keeps the request CORS-safelisted (no preflight) and lets it complete even as the page unloads. The pixel adds no measurable load to your page and ships in well under 10 KB gzipped.
SPA route tracking
For single-page apps (React, Vue, Next.js, SvelteKit, etc.), the pixel automatically tracks client-side navigation. It patches the History API (pushState/replaceState) and listens for popstate (back/forward), then fires a page_view for each new route. Rapid transitions are debounced and duplicate URLs are ignored; hash-only changes are not counted as new views.
Session-scoped attribution (last-click). The lf_click click id and the UTM parameters on the page where a visitor lands are captured as the session origin and stored per-tab in sessionStorage. As the visitor navigates to routes whose URLs carry neither, conversions still attribute to that originating link. A later route carrying a new click id supersedes the stored one, and new UTM parameters supersede the stored UTM (last-click wins) — but the click id is the stronger signal and decides attribution for as long as it stays valid.
Set data-spa="false" to turn this off — the pixel will then send only the initial page view.
Cross-domain tracking
Session attribution is stored per origin, so it does not follow a visitor from one domain to another on its own. If you send visitors from the site that received the click to a second site — a store on a different domain, a booking system, a sister brand — the second site starts a fresh session with no link behind it, and a purchase there is not attributed.
List the hosts you hand visitors to, and the pixel carries the attribution across:
<script src="https://cdn.linkforty.com/pixel.js"
data-workspace="ws_your_site_key"
data-link-domains="store.com, shop.example"
async></script>
When a visitor clicks a link or submits a form pointing at a listed host, the pixel stamps the URL with the session's click id (lf_click) and, as a fallback, its UTM parameters, right before the browser navigates. The pixel on the receiving site reads them from the landing URL exactly as it would after a redirect, so the second session inherits the first one's origin and a conversion there attributes to the original link. It reads the values from the session rather than the current page's URL, so it works however many pages the visitor viewed on the first site.
A few things to know:
- Install the pixel on both sites with the same site key, and list every hostname under the workspace's allowed domains. Only the sending site needs
data-link-domains. - A listed host matches its subdomains —
store.comcoverswww.store.comandcheckout.store.com. Hosts not on the list are never modified, and neither are links back to the page's own hostname — though that own-host check is an exact match: a site onwww.example.comthat listsexample.comto reachshop.example.comwill also decorate its links to the bareexample.com. Harmless (the receiving pixel is the same workspace), but it explains an unexpectedlf_clickon your own URLs. - Deliberate tags are respected.
lf_clickis always set to the session's id; UTM parameters are only added when the destination URL carries none of its own. - The click id stays valid for 7 days after the original click, which covers a normal browse-then-buy journey.
- Server-side redirects on the receiving site can drop the parameters before the pixel runs. WooCommerce's Redirect to the cart page after successful addition is the common case: with it on, a
?add-to-cart=link redirects before the pixel seeslf_click. Land on the product page, or leave that option off. - Navigations the pixel cannot see —
window.open()from your own code, or a URL assembled in JavaScript — are not decorated. Append the parameters yourself in those cases;lf_clickis the one that matters. - Decoration is off under Do Not Track / GPC. The pixel disables itself entirely in that case (see Privacy), so no
lf_clickis appended and the hop isn't attributed. Check your test browser before debugging a missing parameter.
Autocapture
Set data-autocapture="true" to track common interactions without writing any code. Three interaction types are captured, each sent as a conversion:
| Event name | Trigger | Properties |
|---|---|---|
outbound_click | A link to a different host (http/https) | href, host |
file_download | A link with a download attribute or a known file extension (pdf, zip, dmg, csv, docx, mp4, …) | href, ext |
form_submit | Any <form> submission | formId, formName, action (when present) |
Autocapture is off by default. Your own data-lf-event and track() conversions work the same whether or not autocapture is enabled.
Visitor ID (opt-in)
By default the pixel uses a per-tab session ID only, kept in sessionStorage and cleared when the tab closes — there is no durable identifier. Set data-visitors="true" to additionally assign a persistent visitor ID (v_…) stored in localStorage. This powers unique-visitor counts.
A persistent visitor ID is the one durable identifier the pixel can create. Only enable data-visitors="true" where you have the appropriate user consent (GDPR/CCPA). If localStorage is unavailable (private browsing, blocked), no visitor ID is sent and tracking continues without it.
Privacy
The pixel is built to be privacy-respecting by default:
- Do Not Track / GPC — if
navigator.doNotTrackis"1"/"yes", ornavigator.globalPrivacyControlistrue, the pixel disables itself.track()becomes a no-op and no events are sent. - No raw IP storage — the ingestion endpoint uses the request IP only to derive coarse geolocation (country, region, city, timezone) and then discards it. The raw IP is never written to the database.
- Trimmed URLs — query strings and hash fragments are stripped from the page URL and referrer before sending. (UTM parameters are parsed out separately for attribution.)
- Bounded payloads — nested objects in custom properties are dropped, and properties are capped (30 keys, 512 chars per value), so you can't accidentally exfiltrate large or nested PII.
Site key
Your site key identifies your workspace to the ingestion endpoint.
- Format:
ws_followed by 32 hex characters (e.g.ws_a1b2c3d4e5f6789abcdef0123456789a). - Public: it lives in your page's HTML, so treat it as public — it grants the ability to send events to your workspace, nothing more.
- Separate from your App Token. The mobile SDK
appToken(at_…) and the web site key (ws_…) are independent. Rotating one never affects the other — important because an app token is baked into shipped app binaries (effectively non-rotatable), while a web key sits in scrapeable HTML and must be cheaply rotatable.
Rotating the site key
If your key is being abused (spam events from a scraped snippet), rotate it from the dashboard under Workspace Settings → Web Pixel → Rotate site key. Rotation is an owner-only action performed from your logged-in session. The old key stops working immediately, so update your <script> tag with the new key right after rotating.
Custom ingestion domain
By default the pixel sends events to LinkForty Cloud at api.linkforty.com. If you route LinkForty through a custom domain, point the pixel at that domain's ingestion endpoint with data-endpoint:
<script src="https://cdn.linkforty.com/pixel.js"
data-workspace="ws_your_site_key"
data-endpoint="https://app.yourcompany.com/api/pixel/v1/event"
async></script>
Troubleshooting
No events showing up
- Confirm
data-workspaceis your real site key (not thews_…placeholder). - Check the browser console/network tab for a
POSTto…/api/pixel/v1/eventreturning204. - A
204with no row stored usually means an origin allowlist is configured under Workspace Settings and your domain isn't on it. - Verify Do Not Track / GPC isn't enabled in your test browser — the pixel intentionally goes silent when it is.
Conversions not attributed to a link
- Attribution is by the click id first (
lf_click, appended to the destination by every Cloud link unless Click-ID passthrough is turned off for that link), then by UTM. If the link a visitor arrives from has passthrough off, make sure it carriesutm_source/utm_medium/utm_campaignthat match a link in your workspace. See UTM Parameters. - If the conversion happens on a different domain from the one the link landed on, see Cross-domain tracking.
SPA route changes not tracked
- Ensure
data-spaisn't set to"false". The pixel relies on the History API; routers that do full page reloads will fire normalpage_views instead.
Next steps
- Web Analytics dashboard — read the data the pixel produces
- UTM Parameters — how link attribution works
- Mobile SDKs — the app-side counterpart to the pixel