Skip to content

Documentation

The tag

One script tag records page reads, remembers how each visitor arrived, and hears your own signup forms — without cookies, and without touching a form of yours. This is everything it does on its own, everything you can call on it, and what it stores where.

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 Settings → 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 by design: it can only add to your workspace, only from the sites you have listed, and it can read nothing back. Where the tag goes, by platform has the exact steps for Webflow, Framer, WordPress, Squarespace, Carrd and Next.js.


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 — see below

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

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 ever: a record's firstTouch only ever moves earlier, never later. Identity resolution 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 is 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.

The tag does nothing at all while navigator.webdriver is set, so a headless browser crawling 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 Settings → Your website → Catch your signups; 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 did not 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 a sentence kept in Kicklace and hoped to match; else
  3. nothing — and with nothing, 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 submitted twice with the same address inside two seconds — a double click, a framework dispatching its own event — 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. 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 — opting out of being tracked is not 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>

When a post is refused

Kicklace only accepts posts from the addresses you have listed, and a browser hides a refusal from the page that made it — which used to mean a site reported a network problem that was not there. 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
Settings → Your website. https://www.kicklace.com/app/your-workspace/settings/api

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.

When a builder owns its forms

Some site builders — Carrd, Framer, 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 consent shown is consent stored. Link to it, or embed it in an iframe that tells its parent how tall to be. Settings → Lists → your list → Share a signup form has both.