Skip to content

Documentation

The tag

What the script tag does by itself, and what you can call on it.

The tag is the traffic half of Kicklace: what your website knows, as against what your own servers know. It is one line, and it does its work with no cookies, no fingerprint, and no change to a single form of yours.

<script async src="https://www.kicklace.com/api/in/YOUR_PUBLIC_KEY/track.js"></script>

Your own copy, with your own public key already in it, is on Connections → Your website. It goes in the <head> of every page; it works anywhere on the page, because it finds its own address from its src. The key in it is public on purpose: it can only add to your workspace, only from the sites you have listed, and it can read nothing back. Your website and its forms has the steps for each site builder.

What it records on its own

When What is posted
A page loads pageview, with the path
The page navigates without reloading pageview again; pushState, replaceState and the back button are all watched
A link to an installer is clicked download, with the file name (.dmg, .pkg, .exe, .msi, .zip, .apk, .deb, .rpm, .appimage, .tgz, .tar.gz, or any link carrying the download attribute)
Something marked data-kl is clicked that event: data-kl="download", data-kl="cta_click" or data-kl="configure"
One of your forms is submitted a signup, if it looks like one (below)

Every post carries the same few facts about the visit: the path, the referrer's host (only when it's another site; your own pages aren't referrers), a rough platform (win, mac, linux, other), and the browser id.

Excluded pages

If your app or your sign-in pages sit on the same domain as your site, name them under Connections → Your website → Excluded pages, one path per line. Nothing in the table above happens there.

You have to say so, because the tag can't work it out. Once its script is running, a link from your site into your app can leave the same page open underneath, with the tag still in it, so without this it would report every screen your customers open as a visit to your website.

A path covers everything under it: /app takes in /app and /app/settings, and leaves /applications alone. Only what the tag does on its own is affected. kl.track, kl.identify, kl.subscribe and kl.capture still work everywhere, because those are your own code asking.

Where somebody came from, read once

The first page of a session is the one that knows how the session began, and by the third click that information is gone. So the tag reads it once and keeps it for the session:

  • every utm_* parameter in the address, as source, medium, campaign, term, content;
  • an ad click id (gclid, gbraid, wbraid, dclid, msclkid, fbclid, ttclid, twclid, li_fat_id), kept whole, as gclid=…;
  • and, where a click id arrives with no campaign beside it, a source and medium guessed from it (gclid is Google and cpc, fbclid is Facebook and social, and so on).

They travel with every event in the session, so what Kicklace records for a purchase on page nine is still the campaign that brought the person to page one. It is read again the moment a new campaign or click id turns up in the address, because that is a new arrival.

The person's first touch is the one Kicklace keeps for good: a record's firstTouch only ever moves earlier, never later. One person, however they arrive is why that matters when the same person comes back on another device.

The id, and no cookies

One random id lives in localStorage under kl_id. That's the whole of it: no cookie is set, no cookie is read, and nothing about the browser is measured or hashed to recognise it again. The session's arrival facts sit in sessionStorage under kl_touch. Where a browser refuses both, the tag keeps them in memory for the page and carries on.

A browser keeps storage for each address on its own, so www., the bare domain and app. would each hold an id of their own. A link from one of the sites you allow to another carries the id as ?kl_id= for that one click, and the page it lands on takes it up and takes it out of the address bar. It's good for two minutes, so a link somebody copies and sends on later never makes two people one visitor. None of your own sites counts as where somebody came from.

The tag does nothing at all while navigator.webdriver is set, and Kicklace drops a page view from anything whose user agent says it's a search engine, a link preview or a headless browser. A crawler reading your site is not a visitor.

Posts go out with navigator.sendBeacon as text/plain, which means no preflight request and nothing that delays a page as it unloads; where sendBeacon is not there it is fetch with keepalive.

Signup forms it can hear

The tag listens to your forms in the capture phase and never takes one over: no preventDefault, no markup of ours, no ids of ours. Your form validates, submits, and shows its own success exactly as it did before the script was on the page. Kicklace only reads it.

Which forms. A form marked data-kl-list="<your list>" is a signup. So is every form with an email box, if you switch on Catch every signup form under Connections → Your website; that switch needs a list for those people to land in.

Which forms are refused, always. A form carrying a password box (that is a sign-in), a form carrying a search box (that is a search), and a form whose own bot trap has been filled in: a field named hp, honeypot, honey, gotcha or botfield with something in it, which your own form is about to throw away too.

What it reads. The address from the first input[type=email], else from a text input whose name, id, placeholder, autocomplete or label says mail. The name from an input named name, first_name, full_name, your_name or fname; punctuation and case are ignored, so firstName and first-name are the same thing. Nothing else: no password, no message, no field you didn't ask about.

The consent record. This is the part that matters, because it is the proof:

  1. data-kl-consent="…" on the form, if you put it there; else
  2. the words next to a ticked, required checkbox: its own label, as rendered, up to 500 characters, because the sentence somebody actually read beats one kept in Kicklace and hoped to match. A box about your terms or privacy policy is never consent to email, and on a form you didn't mark, a box counts only when its words are about email or news; else
  3. nothing, and then the person is posted to identify instead of subscribe. They exist, with their address, on no list, and nothing may email them until they agree to something.
<form
  data-kl-list="product-updates"
  data-kl-consent="Send me product updates. I can unsubscribe with one click."
>
  <input type="email" name="email" required />
  <button>Sign up</button>
</form>

One submit is one post. The same form sent twice with the same address inside two seconds (a double click, say) is one signup.

Calling it yourself

Everything the tag does on its own, you can do by hand. window.kl is there once the script has loaded:

kl.track("configure", { properties: { finish: "walnut" } });

kl.identify({ email: "ada@example.com" });
kl.identify({ account_id: "acct_1a2b" });

kl.subscribe({
  email: "ada@example.com",
  list: "product-updates",
  consent: "Send me product updates. I can unsubscribe with one click.",
}).then(({ ok, status }) => { /* … */ });

kl.capture(document.querySelector("#signup"));

kl.id();        // this browser's id, for a download token of your own
kl.optOut();    // stop, and forget the id
kl.optIn();     // start again

kl.subscribe and kl.capture resolve to { ok, status } and never throw, so a signup form of your own can show its own success or its own error without a try. A status of confirm_by_email is somebody who had left the list: they were emailed a link, and they're back once they press it. kl.subscribe is refused without consent: missing_consent, and nobody joins a list without the words they agreed to.

kl.optOut() forgets the browser id and the session's arrival facts and stops every post after it. A signup the person then asks for still goes, because opting out of being tracked isn't opting out of a form you filled in on purpose. It simply carries no browser id with it.

To call something before the script has loaded, push it onto a queue and the tag will play it back:

<script>window.klq = window.klq || []; klq.push(["track", "cta_click"]);</script>

The signed-in user

A hosted sign-in, like Clerk's own pages, leaves no form for the tag to hear, so the visitor who has been on your site for a week and the person who just signed in would stay two records. <KicklaceTag user={…} /> writes their address, id and name onto the tag's own script element as data-kl-user-* attributes (never an inline script, so a strict CSP still holds), and the tag reads them the moment it loads and identifies once a session, exactly as kl.identify above does. A plain script tag gets the same by carrying the three attributes by hand:

<script
  async
  src="https://www.kicklace.com/api/in/YOUR_PUBLIC_KEY/track.js"
  data-kl-user-email="ada@example.com"
  data-kl-user-id="user_1a2b"
  data-kl-user-name="Ada Lovelace"
></script>

The address is the signed-in visitor's own, rendered into their own page, and sent only to your workspace's own public ingest route. Nothing is identified for a visitor who isn't signed in. Sign-ups from Clerk and Kinde has why a hosted sign-in needs this, and npx kicklace init writes it for you where it can.

Your public key is on every page, so anybody can post with it. What it says only fills in what a person's record is missing: it never renames somebody, changes the address they already have, or hands their account to another address. A payment, a trial and a cancellation come from your own server or from Stripe, never from the page.

When a post is refused

Kicklace only accepts posts from the addresses you've listed, and a browser hides a refusal from the page that made it. So the tag says it once per page in the console, in the server's own words, with the screen that fixes it:

Kicklace: staging.example.com is not one of your sites. Add it in Kicklace under
Connections → Your website. https://www.kicklace.com/app/your-workspace/connections/website

Your website's own heartbeat, on that same screen, prints every post of the last week in plain English with what happened to it, and a refused row carries a button that allows that exact site in one click. It is the first place to look when something is not arriving.

Kicklace reads your product too

The tag is what your website tells Kicklace as people use it. Separately, Kicklace reads the site itself: the pages a buyer would read, not the traffic on them. Once your website is connected, it fetches your home page, then /sitemap.xml where you have one (an index followed one level into its own sitemaps), and picks the pages a buyer reads first: the home page, then anything about pricing, plans, product, features, sign-up or about, then the rest, up to twenty pages. robots.txt is honoured, for User-agent: Kicklace and for * alike, and Excluded pages above, the same paths the tag itself skips, are left alone here too. Every request carries the user agent Kicklace, sends no cookies, and is a plain GET: never a login form, never a POST, never anywhere but the address you gave Kicklace.

It reads once after a deploy, or once a day where there has been none, and keeps each page's title, its headline, its visible words and the prices on it, never a screenshot and never the markup. What changed since the read before (a price, a new headline, the sentences a page gained or lost) is one line on the deploy's own page, under What changed for a visitor.

When a builder owns its forms

Some site builders, Carrd, Framer and Squarespace among them, own their forms so completely that neither the attribute nor the catch-all switch can hear a submit. For those, Kicklace hosts the form itself, at https://www.kicklace.com/f/<your workspace>/<your list>, wearing your workspace's name and colours. The list's consent sentence is the label of a required tick box, so the consent shown is the consent stored. Link to it, or embed it in an iframe that tells its parent how tall to be. Copy its form, on the list's card on your Lifecycle map, has the address.