Menu

Web SDK

Track signed-in customer activity and register web push with the preview Kirora browser SDK.

Preview

Use the web SDK to track signed-in customer activity for segments and journeys. Record custom events and visits, enable page-view tracking when needed, capture consent and register the browser for push. The SDK is written in TypeScript with no dependencies and can be used with a framework or plain HTML.

How it works

The SDK runs in your users’ browsers, so it never uses an API key. It uses two things instead:

  • A client key (kirora_pk_…): public, safe to put in the page. It says which workspace the events belong to.
  • A user token: signed by your server for the user who is signed in, so nobody can send events as someone else. The SDK asks your server for one at sign-in and whenever one expires.

1 Create your keys

In the dashboard, open Developer → SDK. The workspace owner creates the token secret: put it in your server’s environment as KIRORA_SDK_TOKEN_SECRET. The client key (kirora_pk_…) is shown on the same page and goes in your site.

2 Add a token route to your server

The SDK proves who is signed in with a token from your server. With the Node.js library, the route is one call:

// Express
import { createUserToken } from '@kirora/node';

app.get('/kirora-token', requireSignedIn, (request, response) => {
  response.send(createUserToken(process.env.KIRORA_SDK_TOKEN_SECRET!, request.user.id));
});
// Next.js: app/kirora-token/route.ts
import { createUserToken } from '@kirora/node';
import { auth } from '@/auth';

export async function GET() {
  const session = await auth();
  if (!session?.user?.id) return new Response(null, { status: 401 });
  return new Response(createUserToken(process.env.KIRORA_SDK_TOKEN_SECRET!, session.user.id));
}

In any other language, use a JWT library: sign with HS256 and the token secret, set sub to your own id for the user (their contact’s externalId), and set exp no more than 24 hours after iat.

3 Install and start the SDK

npm install @kirora/web    # or: pnpm add @kirora/web, yarn add @kirora/web

Create one instance for the whole site:

// src/kirora.ts
import { Kirora } from '@kirora/web';

/** Your server's route that signs a token for the signed-in user (step 2). */
export async function fetchKiroraToken(): Promise<string> {
  const response = await fetch('/kirora-token');
  // Throw rather than hand Kirora an error page: the SDK keeps the events and retries.
  if (!response.ok) throw new Error(`Token route answered ${response.status}`);
  return response.text();
}

export const kirora = new Kirora({
  clientKey: 'kirora_pk_…', // public: safe in the page
  fetchUserToken: fetchKiroraToken, // at sign-in, and when a token expires
  trackPageViews: true, // optional
  push: true, // optional: leave out if you send no pushes
});

4 Sign users in and out

import { kirora } from './kirora';

// On every page load where the user is signed in, and right after they sign in.
// The token comes from fetchUserToken; identify(token) takes one you already have.
await kirora.identify();

// Before your own sign-out.
await kirora.logout();

Nothing is sent before sign-in: events tracked earlier wait on the device and go as the user who signs in next. logout() sends what is waiting and forgets the rest.

5 Track what they do

kirora.track('Plan Upgraded', { plan: 'pro', seats: 5 });
kirora.track('Order Completed', { order_id: 'A-1001', total: 49.99, currency: 'USD' });

track() returns at once, since events go in batches, so call it anywhere without await. Name events as the naming guide below describes, so journeys and segments can match them.

Visits, page views and context

The SDK adds what Kirora needs to understand each event, so your own code only names what happened:

  • Context: every event says where it happened, as $ properties beside yours, usable in the same filters: $url, $path, $title, $referrer, $locale, $timezone, any $utm_* campaign tags, $session_id and $device_id, a lasting id for this browser kept until its site data is cleared. The rest of the query string and the fragment are never sent, since they often carry reset tokens and email addresses. Your own properties win on a clash.
  • Visits: a visit begins with a Session Started event that records how the visitor arrived ($referrer, $utm_*), and every event in it carries the same $session_id. So “came from the newsletter, compared plans, left” reads as one visit. A visit ends after 30 minutes without activity, and tabs of the same site share it. Visits are on by default; trackSessions: false turns them off.
  • Page views: trackPageViews: true records Page Viewed on every page, including single-page app navigations (a change to the query string alone is not a new page). It is off by default, since every view is an event. To record page views from your own router instead, call trackPageView().

Where the law asks for consent before tracking, as in the EU and the UK, start the SDK with requireTrackingConsent: true. Nothing is tracked or stored until your consent banner says yes:

export const kirora = new Kirora({ /* … */ requireTrackingConsent: true });

cookieBanner.onAccept(() => kirora.setTrackingConsent(true));
cookieBanner.onReject(() => kirora.setTrackingConsent(false));

The answer is remembered on the device. setTrackingConsent(false) stops tracking and forgets events still waiting. Push consent is separate, below.

Send web push

Push is one channel that uses the same signed-in user. To add it:

  1. Create a web push app in the dashboard. See Set up push.

  2. Start the SDK with push: true.

  3. Serve kirora-sw.js from your site’s root. It must be on your own origin, so copy it into your public folder as part of your build:

    cp node_modules/@kirora/web/dist/kirora-sw.js public/kirora-sw.js

    If your site already has a service worker, add importScripts('https://cdn.jsdelivr.net/npm/@kirora/web/dist/kirora-sw.js') to it instead. It handles only Kirora’s pushes.

There is nothing else to copy: the SDK asks Kirora for the push app’s settings. Ask for permission from a click, since browsers allow the prompt only then:

enableNotificationsButton.addEventListener('click', async () => {
  if ((await kirora.requestPushPermission()) === 'granted') {
    await kirora.setPushConsent(true); // their consent to marketing pushes
  }
});

The browser is registered for push at identify() and when permission is granted. Browser permission alone does not allow marketing pushes: setPushConsent() records the user’s own answer. Opens and clicks are reported by the service worker with no code of yours.

React

Create the instance once, outside any component (step 3), and sign in where your own session starts:

import { useEffect } from 'react';
import { kirora } from './kirora';

export function KiroraSession({ userId }: { userId: string | null }) {
  useEffect(() => {
    if (userId) void kirora.identify();
  }, [userId]);
  return null;
}

// In a component
<button onClick={() => kirora.track('Invite Sent', { channel: 'email' })}>Invite</button>;

Call await kirora.logout() in your sign-out handler, before your own sign-out. React hooks will come as a separate package, @kirora/web-react. (@kirora/react is a different package: the React builders.)

Next.js

The same, from client components ('use client'). Importing the instance during server rendering is harmless: it touches no browser API and sends nothing until identify(), which runs in an effect. Serve kirora-sw.js from public/, which Next.js serves at the site’s root.

Without a bundler

For plain HTML, Webflow or WordPress, put the loader in <head>. It stands in for the SDK until the file arrives, so calls made before then are kept and made in order:

<script>
  (function (options) {
    var kirora = (window.kirora = window.kirora || []);
    if (!Array.isArray(kirora) || kirora.options) return;
    kirora.options = options;
    ['identify', 'logout', 'track', 'trackPageView', 'setPushConsent', 'setTrackingConsent'].forEach(function (method) {
      kirora[method] = function () {
        kirora.push([method, Array.prototype.slice.call(arguments)]);
      };
    });
    var script = document.createElement('script');
    script.async = true;
    script.src = 'https://cdn.jsdelivr.net/npm/@kirora/web/dist/kirora.min.js';
    document.head.appendChild(script);
  })({ clientKey: 'kirora_pk_…', trackPageViews: true });

  kirora.track('Signup Started');
</script>

Once loaded, window.kirora is the SDK itself, and every call the loader held is made on it. A method missing from the loader’s list can’t be called before then. requestPushPermission() is not held: call it from a click once the SDK has loaded.

Methods

Method Does
new Kirora(options) Starts the SDK. clientKey is required. Optional: fetchUserToken, trackSessions (on unless false), trackPageViews, requireTrackingConsent, push (true, or { pushAppId, serviceWorkerUrl }), logLevel (none, error or debug) and apiUrl (https://api.kirora.io unless set).
identify(token?) Signs the user in, with a token from fetchUserToken unless given one; registers the browser for push if allowed; sends waiting events.
logout() Sends what waits, stops this browser’s pushes for the user, and forgets the rest. Safe to call twice.
track(name, properties?) Records an event.
trackPageView() Records Page Viewed for the current page.
requestPushPermission() Shows the browser’s prompt. Answers granted, denied, unanswered or unsupported.
setPushConsent(granted) Records the user’s consent to marketing pushes.
setTrackingConsent(granted) Records the visitor’s answer, with requireTrackingConsent.
flush() Sends waiting events now. Rarely needed.

Delivery and errors

  • Batches: events go at once when 20 are waiting, otherwise every 30 seconds and when the tab is hidden. Up to 1,000 are kept across reloads and tabs.
  • Retries: when the browser is offline or Kirora is busy, the SDK retries with growing gaps, never sooner than the Retry-After Kirora sends. Each event carries an idempotency key, so a resend never counts twice.
  • Errors: Kirora’s refusals are KiroraApiError, with status and code. A failing fetchUserToken is logged as UserTokenError, and the events wait. track() never throws: an event Kirora refuses is logged (see logLevel) and the others still go.

Naming events

Journeys, segments and goals match event names exactly, so settle them once:

  • A thing, then what happened to it, in the past tense and Title Case: Plan Upgraded, Invoice Paid, Signup Started. Not upgrade, clickedButton or PLAN_UPGRADE.
  • What the user did, not the screen it happened on: Order Completed with { "source": "checkout" }, not Checkout Page Button Clicked.
  • Details in properties, not the name: one Plan Upgraded with { "plan": "pro" }, not Pro Plan Upgraded.
  • Commerce events already have names: Product Viewed, Product Added, Order Completed and the rest of the e-commerce events.
  • $ properties belong to the SDK: don’t start your own property names with $.

Names are case-sensitive, and a workspace has at most 1,000 of them: the full rules are in the quickstart.

App SDKs

iOS (Swift), Android (Kotlin), React Native and Flutter SDKs are planned. They share this SDK’s interface: the same options, methods, answers, event names and behaviour, with only each language’s casing and async style changed. Mobile apps record screens with trackScreenView(name) (a Screen Viewed event) instead of page views.

Next steps