@clakta/javascript
The official Clakta SDK for the browser.
Clakta is a modern sales and ads tracking platform built for modern businesses. Get accurate data and know which ads perform best, which influencer sells the most, with a modern and powerful platform.
Website
Framework-agnostic tracking for any website or web app. Zero dependencies, works with a bundler or a single <script> tag.
Installation
npm install @clakta/javascript
pnpm add @clakta/javascript
yarn add @clakta/javascript
Or load it straight from the CDN — no build step required:
<script src="https://cdn.clakta.com/script.js" defer></script>
Pin a specific version if you'd rather control upgrades yourself:
<script src="https://cdn.clakta.com/v1.0.8/script.js" defer></script>
Usage
Nothing is collected until you call init, so call it as early as possible — ideally before your app renders — and pass your script id. Everything else is optional.
import clakta from "@clakta/javascript"
clakta.init({ id: "YOUR_SCRIPT_ID" })
Named imports work too, if you prefer them or want the smaller bundle:
import { init, trackEvent } from "@clakta/javascript"
init({ id: "YOUR_SCRIPT_ID" })
trackEvent("hero-cta", "click")
With a script tag
The CDN build exposes a Clakta global:
<script src="https://cdn.clakta.com/script.js" defer></script>
<script>
window.addEventListener("load", function () {
Clakta.init({ id: "YOUR_SCRIPT_ID" })
})
</script>
To skip the bootstrap entirely, add data-id to the tag and the SDK initializes itself on load:
<script
src="https://cdn.clakta.com/script.js"
data-id="YOUR_SCRIPT_ID"
defer
></script>
If you keep data-id on the tag but want to call init yourself, with options the tag can't express, add data-auto-init="false". Otherwise the tag initializes first and your init call is ignored as a duplicate.
After init runs, the full API is also available on window.clakta — handy for inline scripts, Google Tag Manager, or any code that can't import modules:
<button onclick="window.clakta.trackEvent('hero-cta', 'click')">Buy now</button>
Options
| Option | Type | Default | Description |
|---|---|---|---|
id | string | — | Required. Your Clakta script id. |
apiHost | string | https://api.clakta.com | Override the API host. Tracking/config use https://t.clakta.com by default; Stripe stays on api.clakta.com. |
debug | boolean | false | Verbose console logging. |
consent | boolean | by visitor location | Whether the visitor has consented to first-party identifiers. See Consent. |
How it works
On init the SDK asks the Clakta API how this source should collect data — the tracking mode plus which events are tracked automatically. Those are dashboard settings, so they're resolved at runtime rather than baked into your build.
Two consequences worth knowing:
- Nothing is sent until that config resolves. Calls you make in the meantime (an
identifyright afterinit, say) are buffered and replayed once it does. - If the config can't be resolved — an unknown script id, or an unreachable API — the SDK stays inert and sends nothing at all, rather than guessing how the visitor may be tracked.
Tracking modes
Every project collects data in one of two modes, set in your Clakta dashboard:
| Mode | Behavior |
|---|---|
privacy_friendly | No cookies or first-party identifiers, and no browser storage is touched. The server derives the session. identify is a no-op. |
full_attribution | Storage-backed sessions, a persistent visitor id and richer client context, subject to the visitor's consent. |
The mode isn't a client option — it comes from the project settings so it stays consistent across every surface you install the SDK on.
Under full_attribution with consent, the SDK keeps a single first-party cookie, _clakta, on your registrable domain (with a localStorage copy). It holds the visitor id, which expires a year after it was created — later visits don't extend it — the current session, which ends after 30 minutes of inactivity, and the click token (clkt) the session landed with. The visitor id is what keeps one person counted once when their IP changes mid-visit — routine on mobile networks — or when they move between your subdomains. A second cookie, _clakta_consent, remembers the visitor's answer to your cookie banner; see Consent.
Earlier versions of the SDK kept these values in separate cookies (clakta_session_id, clakta_visitor_id, clakta_has_visited and clakta_landing_page). The first time a consenting visitor returns, their ids are carried over into _clakta and the old cookies are deleted, so nobody is counted as a new visitor.
Consent
Consent only has an effect under full_attribution; in privacy_friendly mode no identifiers are ever used regardless.
Unless you set it, consent follows where the visitor is. Visitors in the EU, the EEA, the UK and Switzerland — and any visitor whose location is unknown — start without consent, as the GDPR and the ePrivacy rules require, and are only identified once your cookie banner calls updateConsent(true). Everyone else starts with it and can still decline. Clakta works out the location from the request when the SDK loads its configuration; nothing about it is stored on the device.
A visitor without consent is still counted, anonymously: nothing is stored on their device, no session or visitor id is sent, and Clakta records the visit the privacy_friendly way — a server-derived session and a visitor id that changes every day. Everything else is still recorded: page views with their UTM tags, clkt and ad click ids (gclid, fbclid, …), clicks, scrolls and form submissions, and the shopping funnel — pageViewWithProduct, addToCart, initiateCheckout and purchase — with only the customer details left out.
Set the starting value yourself at init — false to ask every visitor wherever they are, or true if the SDK only loads once the visitor has accepted:
clakta.init({ id: "YOUR_SCRIPT_ID", consent: false })
Or, on the script tag, with data-consent:
<script
src="https://cdn.clakta.com/script.js"
data-id="YOUR_SCRIPT_ID"
data-consent="false"
defer
></script>
And update it when the visitor makes a choice in your cookie banner:
import { getConsent, updateConsent } from "@clakta/javascript"
updateConsent(true) // or clakta.consent.update(true)
getConsent() // => true
getConsent() reports false until the choice is made — by you, by the visitor, or by their location once the configuration has loaded.
What happens when the visitor answers
The answer is remembered in a _clakta_consent cookie on your registrable domain, shared by every subdomain and kept for a year from the latest answer, and in a localStorage copy with no expiry, which keeps it on the site it was given on after the cookie is gone. Later pages apply it from their first event, so your banner only has to report the answer once, when the visitor gives it; reporting it again on every page is harmless and renews the cookie. A refusal is remembered too — that is what honouring it on the next page takes — and it is the one thing the SDK stores without consent. Nothing is stored for privacy_friendly sources.
Your own consent value outranks a remembered answer. true says the visitor accepted. false keeps every visitor anonymous, whatever they answered before, until they accept on the current page — so with false, your banner has to call updateConsent(true) (or set window.claktaConsent) on every page for visitors who accepted.
When a visitor accepts mid-visit, the SDK starts their identified session right away and records the current page in it. The session takes its click token (clkt) from the URL this page load started on — the one the visitor arrived with from your ad — so a sale that follows keeps its ad source. That page view also names the anonymous view of the same page, and Clakta drops the anonymous one so the page is counted once: page views from visitors who haven't answered the banner yet are held for 60 seconds before they reach your dashboards, which covers most banner answers. Page views from visitors who refused aren't held. A visitor who answers later has that page counted in both sessions. Pages viewed before the page they accepted on stay in the anonymous part of the visit, since nothing could be stored for them without consent.
A banner can answer before a deferred script tag has loaded, when clakta.consent doesn't exist yet. Set window.claktaConsent instead, and init picks it up over everything else — the starting value, a remembered answer and the visitor's location:
window.claktaConsent = true
Revoking consent immediately clears every identifier the SDK stored, and subsequent events are recorded anonymously as above.
Personal data in URLs
Every event carries the page URL, so values in it that name a person or work as a credential are replaced with redacted before they leave the browser, whatever the visitor's consent: any value that contains an email address, and parameters such as email, phone, token, password or first_name. The same applies to the referrer, outbound links and form actions. URLs with nothing to replace are sent exactly as they are.
Automatic tracking
Once initialized, the SDK installs only the listeners your project has enabled in the dashboard:
- Page views — tracked on load and on every SPA navigation (
pushState,replaceState,popstate,hashchange). Repeat views of the same URL are debounced. - Time on page — counted only while the page is visible, and reported when the visitor hides the tab, closes the page or navigates within your app, so the last page of a visit is timed too. Comes with page views; it has no switch of its own.
- Clicks — outbound links, plus any element you explicitly opt in with
data-clkt-track-click. Clicks elsewhere are ignored, so you don't get an event for every click on the page. - Scroll depth — 25% / 50% / 75% / 100% milestones, with the furthest depth reached emitted on exit for short pages.
- Form submissions — form id, action, and field count. Field values are never collected, in either tracking mode.
<button data-clkt-track-click id="hero-cta">Buy now</button>
Everything below can be called manually whether or not automatic tracking is on.
API
pageView()
Records a page view for the current URL. Useful when automatic page views are off, or for virtual page views your framework doesn't route through the History API.
clakta.pageView()
trackEvent(trackingId, eventType, metadata?)
Tracks a custom event in the current session.
clakta.trackEvent("hero-cta", "click", { variant: "A" })
clakta.trackEvent("onboarding", "signup", { plan: "pro" })
identify(customer)
Attaches customer details to the session — call it when you learn who the visitor is, e.g. on login or signup. At least one of externalId, claktaCustomerId, or email is required; any extra fields are forwarded as free-form traits.
clakta.identify({
externalId: "user_123",
email: "[email protected]",
name: "Ada Lovelace",
plan: "pro", // extra fields become traits
})
Identifying a visitor is the opposite of anonymous collection, so this is a no-op under privacy_friendly and when consent has been withheld.
Ecommerce
Three funnel events cover the path to a sale. Each keeps anything extra you pass as metadata, so you can hand it the shape your platform already uses.
// Add to cart — no order exists yet
clakta.addToCart({
products: [{ id: "42", name: "Tee", price: 29.99, quantity: 2 }],
totalPrice: 59.98,
currency: "USD",
})
// Checkout started
clakta.initiateCheckout({
orderId: "1001",
totalPrice: 59.98,
currency: "USD",
products: [{ id: "42", name: "Tee", price: 29.99, quantity: 2 }],
customer: { email: "[email protected]" },
})
// Purchase completed
clakta.purchase({
orderId: "1001",
totalPrice: 59.98,
currency: "USD",
paymentMethod: "credit_card",
products: [{ id: "42", name: "Tee", price: 29.99, quantity: 2 }],
customer: { email: "[email protected]" },
// Optional — clkt is read from the URL when you don't pass one
tracking: { utm_source: "facebook", utm_medium: "cpc" },
})
There's also pageViewWithProduct({ id, name, price, currency }) for product detail page views.
The order is identified, not defined
initiateCheckout and purchase require orderId — the order's id in your own database, and the same value your server sends to the Orders API. Calls without one are dropped with a console warning.
That id is the only thing the browser event really has to get right. Clakta reconciles it against the order your server posts, and the server's order stays the source of truth for status, totals and contents. Two consequences worth designing around:
- A visitor who closes the tab before your thank-you page renders costs you nothing — the server-side order still lands.
- The client can't invent or amend an order. Fields the server owns (
projectId,platform,status, timestamps) aren't accepted here at all.
The rest is a small identifying subset of the Orders API payload, using the same field names so both sides describe the order the same way:
| Field | Type | Notes |
|---|---|---|
orderId | string | number | Required. Numbers are normalized to strings. |
totalPrice | number | Decimal, e.g. 59.98. |
currency | string | ISO 4217. Defaults to the project currency. |
paymentMethod | ClaktaPaymentMethod | credit_card, pix, paypal, apple_pay, … (the Orders API enum). |
products | { id, name, quantity, price }[] | Product ids are normalized to strings. |
customer | { email, phone, name } | Identifiers only. See below. |
tracking | ClaktaTrackingParams | Attribution parameters. See below. |
customer is personal data, so it's sent only under full_attribution with consent granted — in privacy_friendly mode the block is dropped from the payload entirely, the same way identify is a no-op there. The event itself is still recorded: orderId alone is enough to reconcile it.
addToCart takes the same products / totalPrice / currency fields but no orderId, since the order doesn't exist yet.
Attribution
initiateCheckout and purchase accept an optional tracking block — the same one your server sends to the Orders API, so both sides credit the order to the same click:
| Field | Type | Notes |
|---|---|---|
clkt | string | The Clakta click token. Resolved from the URL when omitted — see below. |
utm_source | string | e.g. "facebook". |
utm_medium | string | e.g. "cpc". |
utm_campaign | string | e.g. "summer-sale". |
utm_content | string | e.g. "video-a". |
clakta.purchase({
orderId: "1001",
totalPrice: 59.98,
tracking: {
clkt: "meta:facebook:123456:987654:555555", // optional
utm_source: "facebook",
utm_medium: "cpc",
utm_campaign: "summer-sale",
utm_content: "video-a",
},
})
Every field is optional — an order sent without any of them is recorded as unattributed — and anything extra you pass is kept as metadata.
clkt is the one value the SDK can recover on its own, because it usually isn't there to pass: the token arrives on the ad's landing URL, and by the time the visitor reaches checkout the query string is long gone. So when you omit it, the SDK looks for ?clkt= on the current URL, then on the URL the visit landed with. That fallback reads stored state, so it applies under full_attribution with consent granted; in privacy_friendly mode no storage is touched and only the current URL is checked. Pass clkt yourself — from your own server-side record of the order, say — and your value is used as-is.
decorateLemonSqueezyUrl(url)
Lemon Squeezy checkout links can carry the visitor's session, and the SDK adds it for you: when someone clicks a link to *.lemonsqueezy.com/checkout/buy/… or /buy/…, it appends checkout[custom][clakta_session_id] (and checkout[custom][clkt] when the visit carries a click token) before the click goes through. Lemon Squeezy stores that permanently against the order and sends it back on the webhook, so Clakta credits the sale to the visitor who made it. Hosted checkouts and Lemon.js overlay buttons are both covered, and nothing is added to a single-use ?cart= URL or to a key you already set yourself.
You only need this function for a checkout you open yourself:
LemonSqueezy.Url.Open(
clakta.decorateLemonSqueezyUrl("https://acme.lemonsqueezy.com/buy/abc-123"),
)
Building checkouts through Lemon Squeezy's API instead? Pass the session in checkout_data.custom when you create them — it's the same key, and it's the most reliable option because it never depends on the browser:
custom: {
clakta_session_id: claktaSessionId
}
The URL is returned untouched in privacy_friendly mode and when consent was declined: those visitors have no client-side id, and Clakta derives their identity server-side instead.
decoratePaddleCheckout(options)
Paddle checkouts carry the visitor's session in customData, and the SDK adds it for you — both ways a checkout can open:
- HTML checkout buttons. When someone clicks an element with Paddle's
paddle_buttonclass, the SDK mergesclakta_session_id(andclkt, when the visit carries a click token) into itsdata-custom-databefore Paddle.js reads it. Paddle.Checkout.open(). The SDK wraps the method once Paddle.js loads, so a programmatic overlay or inline checkout is covered with no code of your own.
This is worth more on Paddle than anywhere else: Paddle copies a transaction's custom data onto the subscription it creates, and from there onto every renewal — so one decoration at checkout attributes a subscription's whole lifetime.
You only need the function itself when you build the options object somewhere the SDK can't see it:
Paddle.Checkout.open(
clakta.decoratePaddleCheckout({
items: [{ priceId: "pri_01gm81eqze2vmmvhpjg13bfeqg", quantity: 1 }],
}),
)
Creating transactions through Paddle's API instead? Pass the session in custom_data when you create them — it's the same key, and it's the most reliable option because it never depends on the browser:
custom_data: {
clakta_session_id: claktaSessionId
}
Keys you already set yourself are never overwritten, and the options are returned untouched in privacy_friendly mode and when consent was declined: those visitors have no client-side id, and Clakta derives their identity server-side instead.
decorateCaktoUrl(url)
Cakto checkout links can carry the visitor's session, and the SDK adds it for you: when someone clicks a link to pay.cakto.com.br/…, it appends ?sck= with the session id before the click goes through. Cakto stores that against the order and reports it back on the webhook, so Clakta credits the sale to the visitor who made it.
Cakto also keeps the value in a cookie on its own domain and applies it to the purchase that follows, so decorating the link a visitor clicks is enough — they can move around the checkout and still be credited.
You only need the function itself for a URL you build somewhere the SDK can't see it, such as a server-side redirect or a router link:
window.location.href = clakta.decorateCaktoUrl(
"https://pay.cakto.com.br/AA58c2n",
)
sck is the only field Cakto offers, so unlike Lemon Squeezy and Paddle the click token doesn't ride along — it's already recorded against the session server-side. Cakto's other parameter, src, is deliberately left alone: it never reaches the webhook, so writing to it would be invisible to Clakta while trampling a value you may be using. An sck you set yourself is never overwritten, and the URL is returned untouched in privacy_friendly mode and when consent was declined: those visitors have no client-side id, and Clakta derives their identity server-side instead.
decorateKiwifyUrl(url)
Kiwify checkout links can carry the visitor's session, and the SDK adds it for you: when someone clicks a link to pay.kiwify.com.br/… (or pay.kiwify.com/…), it appends ?sck= with the session id before the click goes through. Kiwify stores that against the order and reports it back inside TrackingParameters on the webhook, so Clakta credits the sale to the visitor who made it.
You only need the function itself for a URL you build somewhere the SDK can't see it, such as a server-side redirect or a router link:
window.location.href = clakta.decorateKiwifyUrl(
"https://pay.kiwify.com.br/dF32ZQ8",
)
sck is the only field Kiwify offers, so as with Cakto the click token doesn't ride along — it's already recorded against the session server-side. Kiwify's other free-form slot, src, is deliberately left alone: merchants use it for their own source labels and read it back in Kiwify's own reporting. An sck you set yourself is never overwritten, and the URL is returned untouched in privacy_friendly mode and when consent was declined: those visitors have no client-side id, and Clakta derives their identity server-side instead.
Decoration is matched on Kiwify's own checkout hosts, so a checkout served from a custom domain of yours is returned untouched — append sck yourself in that case, using clakta.getSessionId().
decorateHotmartUrl(url)
Hotmart checkout links can carry the visitor's session, and the SDK adds it for you: when someone clicks a link to pay.hotmart.com/…, it appends ?sck= with the session id before the click goes through. Hotmart stores that against the purchase and reports it back on the order, so Clakta credits the sale to the visitor who made it.
Hotlinks are decorated too. go.hotmart.com/… is the domain most producers and affiliates actually share, and it forwards the tracking parameters it receives on to the checkout it lands on — so a sale that starts from a shared hotlink is attributed the same way one starting at pay. is.
You only need the function itself for a URL you build somewhere the SDK can't see it, such as a server-side redirect or a router link:
window.location.href = clakta.decorateHotmartUrl(
"https://pay.hotmart.com/P12345678X",
)
sck is the only field Clakta writes, so as with Cakto and Kiwify the click token doesn't ride along — it's already recorded against the session server-side. Hotmart's other parameters are deliberately left alone: src is the source label you already use in Hotmart's own reporting, and xcod carries Hotmart's affiliate hotlink code, so overwriting it could cost an affiliate their commission on the sale. An sck you set yourself is never overwritten, and the URL is returned untouched in privacy_friendly mode and when consent was declined: those visitors have no client-side id, and Clakta derives their identity server-side instead.
Decoration is matched on Hotmart's own hosts, so a checkout served from a custom domain of yours is returned untouched — append sck yourself in that case, using clakta.getSessionId().
consent.update(enabled) / getConsent()
See Consent.
TypeScript
The package ships its own types — no @types/* needed.
import clakta, {
type ClaktaCartDetails,
type ClaktaOptions,
type ClaktaOrderCustomer,
type ClaktaOrderDetails,
type ClaktaPaymentMethod,
type ClaktaProduct,
type ClaktaTrackingParams,
type CustomerData,
type ResolvedConfig,
type TrackingMode,
} from "@clakta/javascript"
Server-side rendering
Every entry point is browser-only and no-ops safely when window is undefined, so importing the package in an SSR build won't throw. Calls made on the server simply do nothing.
Links
Tracking transport regression test
From the repository root, run pnpm dlx [email protected] --test packages/javascript/test/ingestion.test.ts. This checks fetch/sendBeacon routing, unchanged payload session identifiers, custom hosts, and Stripe checkout isolation.