API/Attribution and Events

Attribution and Events

Install first-party attribution, identify customers, and ingest trusted conversions or revenue.

Kliq connects redirects, external customer identifiers, events, conversions, and revenue through first-party first-touch attribution. Browser events use a publishable site key. Trusted server events use a secret Admin API key. Never put an Admin API key in browser code.

Create a tracking site

Open Organization → Attribution, then create one tracking site for each destination application or group of applications that share the same allowed origins.

  • Give the site a recognizable name.
  • Add every production hostname that may send browser events. Enter hostnames without paths; full URLs are accepted and normalized.
  • Add preview, staging, or local hostnames explicitly when they need tracking. Wildcards are not accepted.
  • Hostnames match exactly. example.com and www.example.com are two separate entries: listing only the apex silently rejects every event from the www host with forbidden_origin. Add both whenever both resolve.
  • Choose an attribution window from 7 to 90 days. The default is 30 days.

A site accepts up to 20 hostnames. When a visitor lands on one domain and converts on another, read Multi-Domain Attribution before deciding whether to create a second site.

Kliq creates a publishable key and a copy-ready configuration snippet immediately. The key is intentionally public and allowed origins protect ordinary browser use; they are not proof that an event is trusted. Treat browser events as product telemetry, and send conversions, entitlements, and revenue from your authenticated backend. Disable the tracking site to revoke browser ingestion without deleting historical attribution.

Install the browser SDK

Copy the generated snippet from Organization → Attribution. The hosted, framework-free client works in any browser application without a package-registry login or build dependency:

<script src="https://kliq.sh/kliq.js"></script>
<script>
  window.kliq = Kliq.create({
    endpoint: "https://kliq.sh/tracking",
    publishableKey: "kliq_pk_YOUR_PUBLISHABLE_KEY",
    attributionWindowDays: 30,
  });
</script>

Kliq.create also publishes the instance as window.kliq, so the rest of your application can reach it. This matters in a bundled application: a const declared in the install snippet lives in that classic script's scope, which your components cannot see. Always read the client as window.kliq.

The endpoint is https://kliq.sh/tracking, a first-party path on Kliq itself that forwards to the tracking backend. Keep it as generated; the backend host is an implementation detail and may change.

By default, initialization captures lf_id from the current URL, stores it in first-party localStorage, and removes only that parameter from the visible URL. Other query parameters and the URL fragment are preserved.

Load the script in the browser before initializing it. You can also pass a compatible storage implementation. Set captureOnInit: false only when the application calls kliq.capture() itself.

Guard your integration against the script failing to load — a blocked request, a content blocker, or a stale URL leaves window.Kliq undefined:

const clickId = window.kliq?.getClickId() ?? readClickIdFromUrl();

Reading lf_id from location.search as a fallback keeps the landing a visitor just arrived on attributable even when the SDK never initialized. The SDK strips the parameter once it has stored it, so the fallback only fires when nothing else did.

The conversion funnel

Kliq models attribution as three steps, in the same order as the visit itself:

  1. Click — the redirect records a click and hands lf_id to the destination application, where the SDK stores it first-party.
  2. Lead — the first moment you learn who the visitor is. An email from a form is enough; a signed-in user ID is better. The lead binds the stored click to a customer.
  3. Sale — revenue for a customer that already has a lead. Every sale inherits the link attribution the lead established.

Only the lead step needs the click ID, so it must run in the browser (or receive a clickId you forwarded to your backend). Sales are pure server calls keyed by the customer.

Capture a lead

Use lead when the application does not have a user ID yet — a newsletter form, a waitlist, a demo request. The email becomes the customer key.

await kliq.lead("melvyn@example.com", {
  name: "Melvyn",
  eventName: "Sign up",
  metadata: { form: "newsletter" },
});

trackLead is the full form when you have both an email and your own ID:

await kliq.trackLead({
  externalCustomerId: currentUser.id,
  customerEmail: currentUser.email,
  customerName: currentUser.name,
  eventName: "Trial started",
  idempotencyKey: `lead:${currentUser.id}`,
});

eventName defaults to Sign up. When externalCustomerId is omitted the normalized email is used as the customer key.

lead(email) then identify(userId) is the supported path. The SDK sends the stored email as previousCustomerKey so Kliq rekeys that row onto your user id instead of creating a second person. If both rows already exist (two devices, identify without the stored email), they are merged into the user id. A later lead(email) does not overwrite the stored user id.

A brand-new browser lead still needs a real same-org click ID. After that first lead, identify may run without a click if the attribution cookie has expired.

Identify a customer

Call identify after the destination application knows its stable internal customer identifier. Prefer an internal ID over an email here; use lead when the email is all you have.

const result = await kliq.identify(currentUser.id, {
  idempotencyKey: `identify:${currentUser.id}`,
});

if (!result.ok) {
  console.error(result.code, result.retryAfter);
}

The SDK attaches the stored click when one is still in the window. If the click cookie has expired but lead(email) already created the customer, identify still rekeys that row.

Track a browser event

await kliq.track("signup.completed", {
  metadata: {
    plan: "pro",
    source: "onboarding",
  },
  idempotencyKey: `signup.completed:${currentUser.id}`,
});

Browser ingestion accepts named product events, but it deliberately rejects trusted conversion revenue. Send conversions and money from a server.

Verify the installation

  1. Open an active short link in a new browser session.
  2. Confirm the destination initially receives lf_id and the SDK then removes it from the visible address.
  3. Confirm kliq:attribution exists in first-party local storage.
  4. Sign in or complete the identified action in the destination application.
  5. Open the Kliq parent link. Its detail page shows whether attribution is configured, and its Analytics page shows identified users and events.
  6. Open Customers to inspect the attributed journey.

An invalid_key response means the key does not exist or the site is disabled. A forbidden_origin response means the request's exact hostname is missing from the tracking site. A rate_limited response includes retryAfter in milliseconds.

The customer reached Kliq, so ingestion works; the click ID did not travel with them. Server leads are trusted and accept a missing click rather than failing, which is what turns a broken browser install into a quiet stream of unattributed customers. Work backwards:

  1. Is the SDK loaded? Check window.Kliq and window.kliq in the console on the destination page. If window.Kliq is undefined the script itself never loaded — usually a stale or wrong src.
  2. Did the click store? Look for kliq:attribution in that origin's local storage after opening a short link.
  3. Is the hostname allowed? A browser lead from an unlisted host, including a www variant you forgot to add, returns forbidden_origin.
  4. Does your server call forward the click? If leads are sent from your backend, confirm it receives kliq.getClickId() from the browser and passes it as clickId.

Clicks recorded with zero identified users on the Analytics page point at the destination install, not at the link.

List customers

curl "https://kliq.sh/api/v1/customers?limit=50" \
  -H "Authorization: Bearer $KLIQ_API_KEY"

Add linkId=LINK_ID to return customers currently attributed to one link.

Read a customer journey

The path uses your application's stable external customer ID, URL encoded when necessary:

curl "https://kliq.sh/api/v1/customers/customer_123" \
  -H "Authorization: Bearer $KLIQ_API_KEY"

The response includes the customer aggregate and their events in descending order with cursor pagination.

Track a lead from the server

When the signup happens on your backend, forward the click ID your frontend captured (kliq.getClickId()) and record the lead with an Admin API key:

curl -X POST https://kliq.sh/api/v1/track/lead \
  -H "Authorization: Bearer $KLIQ_API_KEY" \
  -H "Content-Type: application/json" \
  --data '{
    "clickId": "CLICK_ID",
    "eventName": "Sign up",
    "externalCustomerId": "customer_123",
    "customerEmail": "melvyn@example.com",
    "customerName": "Melvyn",
    "idempotencyKey": "signup-customer_123"
  }'

Send customerEmail alone when you have no user ID. Unlike browser leads, server leads are trusted: a lead without a click ID is stored as an unattributed customer instead of being rejected.

Track a sale

curl -X POST https://kliq.sh/api/v1/track/sale \
  -H "Authorization: Bearer $KLIQ_API_KEY" \
  -H "Content-Type: application/json" \
  --data '{
    "externalCustomerId": "customer_123",
    "eventName": "Subscription created",
    "amount": 9900,
    "currency": "EUR",
    "invoiceId": "in_123",
    "metadata": {"plan":"pro"}
  }'

amount uses minor currency units, so 9900 EUR represents €99.00. currency defaults to USD and eventName defaults to Purchase. invoiceId doubles as the idempotency key, so replaying the same invoice returns the original event instead of double-counting revenue. Pass an explicit idempotencyKey when your billing system has no invoice.

The sale inherits the first-touch attribution the lead established. Trusted installments after the click window stay on that same click.

Coming from Dub

The funnel is the same; the field names differ.

DubKliq
dub_id query parameterlf_id query parameter
POST /track/leadPOST /api/v1/track/lead
POST /track/salePOST /api/v1/track/sale
customerExternalIdexternalCustomerId
customerName / customerEmailsame
amount (minor units)amount (minor units)
invoiceId (idempotency)invoiceId (idempotency)
dubAnalytics.trackLead()kliq.trackLead()
publishable key + allowed hostspublishable key + allowed hosts

Kliq has no browser-side sale call by design: revenue is only accepted from a server holding an Admin API key.

Ingest a trusted conversion

Trusted conversions and revenue must be sent from a server with an Admin API key created under Organization → API Keys, never from browser code.

curl -X POST https://kliq.sh/api/v1/events \
  -H "Authorization: Bearer $KLIQ_API_KEY" \
  -H "Content-Type: application/json" \
  --data '{
    "externalCustomerId": "customer_123",
    "clickId": "CLICK_ID",
    "eventName": "subscription.created",
    "kind": "conversion",
    "revenueMinor": 9900,
    "currency": "EUR",
    "metadata": {"plan":"pro"},
    "idempotencyKey": "subscription-sub_123-created"
  }'

If clickId is absent or outside the attribution window, Kliq keeps a valid customer event without fabricating a link attribution.

Reuse a stable idempotencyKey for retries of the same business event. Revenue uses minor currency units, so 9900 EUR represents €99.00.

Audit recent events

curl "https://kliq.sh/api/v1/events?limit=100" \
  -H "Authorization: Bearer $KLIQ_API_KEY"
Analytics APIDomains and Imports API