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, assource,medium,campaign,term,content; - an ad click id (
gclid,gbraid,wbraid,dclid,msclkid,fbclid,ttclid,twclid,li_fat_id), kept whole, asgclid=…; - and, where a click id arrives with no campaign beside it, a source and medium guessed from it
(
gclidis Google andcpc,fbclidis 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:
data-kl-consent="…"on the form, if you put it there; else- 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
- nothing, and then the person is posted to
identifyinstead ofsubscribe. 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.