XeniaXenia Docs

JavaScript SDK

PreviousNext

Install @xenia-martech/sdk-js and connect any web stack to Xenia, from a plain script tag to Next.js server components.

@xenia-martech/sdk-js is the official Xenia SDK for web channels. Wrap your app once, mark up a few elements, and the SDK handles identity, journey attribution, event delivery, in-page personalization, and analytics for you.

What the SDK does

One client does four jobs, all sharing the same identity and attribution state:

JobWhat it doesCall it with
Event trackingRecords an event and, optionally, starts or advances a journey. Identity, UTMs and page context ride along.data-xenia attributes, track(), <XeniaTrack>
Webhook request and responseFires a journey and gets its reply. Actions in the reply (banner, popup, swap, redirect) render on the page.request(), useWebhook(), webhook() on the server
PersonalizationFetches the right content for a content key, with the default as a stable fallback.resolve(), usePersonalizationResolve()
Analytics and conversionsBatches consent-gated analytics events and records purchases against the journey that earned them.track(), identify(), consent()

Install

npm install @xenia-martech/sdk-js

react and react-dom 18 or later are optional peers, needed only for the React bindings. next is an optional peer for the Next.js helpers.

No package manager? Load the prebuilt bundle from a CDN instead. Pin the major version in production so a future release cannot change your site unannounced:

<script src="https://cdn.jsdelivr.net/npm/@xenia-martech/sdk-js@1/dist/xenia-sdk.iife.js"></script>

Pick your entry point

Your stackImport
React, client-rendered or SSR-hydrated@xenia-martech/sdk-js/client/react
Vue, Angular, Svelte, or plain JavaScript@xenia-martech/sdk-js/client
Static HTML, a CMS template, no bundlerThe <script> bundle, window.Xenia
Node backends, server actions, React Server Components@xenia-martech/sdk-js/server
Next.js 15+ App Router@xenia-martech/sdk-js/next with /server

A mostly server-rendered app that needs one interactive widget uses /server for the page and mounts the React provider inside just that one component. See a scoped client island.

Quick start

Every client needs your workspace id (tenantId) and environment key (environmentKey). Both are shown in the app next to your API tokens. Each environment has its own values, so staging and live never mix.

Wrap your app once. Identity, attribution and data-xenia auto-tracking start automatically.

import { XeniaProvider, useTrack } from "@xenia-martech/sdk-js/client/react";
 
export default function App() {
  return (
    <XeniaProvider tenantId="<workspace id>" environmentKey="<environment>">
      <button data-xenia="checkout&click">Checkout</button>
      <YourApp />
    </XeniaProvider>
  );
}
 
function AddToCart({ id }: { id: string }) {
  const track = useTrack();
  return (
    <button onClick={() => track("add_to_cart", { productId: id }, { trigger: true })}>
      Add to cart
    </button>
  );
}

Tokens

Create an SDK token in the app under API Tokens. It belongs to one workspace and one environment. Tracking events works without one, unless the journey's trigger requires auth. Personalization always requires one.

The SDK ships that route ready-made:

// app/api/xenia-token/route.ts (any framework with route handlers works the same way)
import { createTokenRoute } from "@xenia-martech/sdk-js/server";
 
export const GET = createTokenRoute({ sdkToken: process.env.XENIA_SDK_TOKEN! });

Then point the browser client at it with getToken. The SDK caches the token and fetches a new one shortly before it expires.

<XeniaProvider
  tenantId="<workspace id>"
  environmentKey="<environment>"
  getToken={() => fetch("/api/xenia-token").then((r) => r.json()).then((r) => r.token)}
>

Not on a framework with route handlers? Call refreshSdkToken() from any HTTP handler (Express, Fastify, a Lambda) to do the same exchange.

Server-side

The server client is stateless: there is no window, so you bind each request's visitor and page once, and every call on that client carries them.

import { createXeniaServer, getVisitorId } from "@xenia-martech/sdk-js/server";
import { contextFromUrl } from "@xenia-martech/sdk-js";
 
const xenia = createXeniaServer({
  tenantId: process.env.XENIA_TENANT_ID!,
  environmentKey: process.env.XENIA_ENV_KEY!,
  token: process.env.XENIA_SDK_TOKEN,
  visitorId: getVisitorId(request.headers.get("cookie")) ?? undefined,
  context: contextFromUrl(request.url, request.headers.get("referer") ?? undefined),
});
 
const offer = await xenia.webhook("<trigger id>", { source: "backend" }, { fallback: null });
const hero = await xenia.resolve("homepage-hero", { merge: true, fallback: null });

The browser SDK writes the visitor id to a first-party xenia_vid cookie. Reading it back with getVisitorId() is what lets a server-side purchase be credited to a journey the shopper entered in the browser.

Next.js

@xenia-martech/sdk-js/next replaces the cookie and URL plumbing with two calls.

// proxy.ts (middleware.ts before Next 16)
import type { NextRequest } from "next/server";
import { xeniaProxy } from "@xenia-martech/sdk-js/next";
 
// Mints the xenia_vid cookie for new visitors and stamps the page URL on the request
export const proxy = (request: NextRequest) => xeniaProxy(request);
// Any Server Component, Server Action or Route Handler
import { consentFromNext, xeniaServerFromNext } from "@xenia-martech/sdk-js/next";
 
const xenia = await xeniaServerFromNext(config); // bound to this visitor and page
const res = await xenia.webhook("<trigger id>", {}, { fallback: null });
 
// In a Server Action, after a form submit
const consent = await consentFromNext();
if (consent.analytics) {
  await xenia.identify({ email });
  await xenia.track("formSubmitted", { eventId: `contact-${ref}`, consent });
}

Running your own consent banner without the browser SDK? Write the cookie the server reads with consentCookie():

import { consentCookie } from "@xenia-martech/sdk-js";
 
document.cookie = consentCookie({ analytics: true, consentSource: "banner" });

A scoped client island

In a server-rendered app, mount the provider inside the one interactive component rather than at the root. Its getToken calls your token route.

"use client";
import { XeniaProvider, useWebhook } from "@xenia-martech/sdk-js/client/react";
 
function OfferButton({ triggerId }: { triggerId: string }) {
  const { send, isLoading } = useWebhook(triggerId);
  return (
    <button onClick={() => void send({ page: "storefront" })} disabled={isLoading}>
      {isLoading ? "Finding your offer…" : "Reveal my offer"}
    </button>
  );
}
 
export function WelcomeOffer({ triggerId }: { triggerId: string }) {
  return (
    <XeniaProvider tenantId="<workspace id>" environmentKey="<environment>" getToken={getToken}>
      <OfferButton triggerId={triggerId} />
    </XeniaProvider>
  );
}

Configuration

These options apply to XeniaProvider, createXeniaClient, createXeniaServer and Xenia.init.

OptionDefaultNotes
tenantIdYour workspace id. Required.
environmentKeyYour environment key. Required.
tokenSDK token. Required for personalization, and for triggers that require auth.
getTokenAsync token provider, cached until shortly before expiry. Overrides token.
baseUrlhttps://engine.xeniamartech.com/xeniaChange only for a non-default environment.
visitorIdSeeds the visitor id in the browser. On the server, binds the client to one visitor.
visitorCookietrueBrowser only. Writes the xenia_vid cookie your server reads. false disables it.
prefix"xenia"Storage keys, the data-xenia attribute, and cookie names. Pass the same prefix to the server helpers.
timeout8000Per-request timeout in milliseconds.
retry{ attempts: 2, baseDelayMs: 300 }Retries transient failures with backoff and honours Retry-After. Webhooks never retry.
circuitBreakeroffAfter 3 consecutive failures on one URL, calls fail instantly for 30 seconds, then one call probes.
headersExtra headers on every request.
consentBrowser only. Initial consent flags.
debugfalseVerbose console diagnostics.

Browser-only options:

OptionDefaultNotes
autoTracktrueTurns data-xenia auto-tracking on or off.
throttleMs1000Throttle window for auto-tracked events.
viewport{ threshold: 0.5, triggerOnce: true }Options for view-port tracking.
autoApplyActionstrueApplies actions from webhook replies automatically.
persistResponsestrueKeeps webhook replies in localStorage.
respectDnttrueHonours Do Not Track and Global Privacy Control for analytics-only events. Journey triggers always fire.
messageTemplate, messageComponent, messageStylesReplace or restyle the built-in message card. See Webhooks and actions.

Failing safely

Nothing the SDK does blocks your render. Requests time out and retry with backoff, and a hook that fails leaves data as null so your default content keeps showing. Server-side, pass { fallback: null } to webhook(), resolve() or resolveBatch() and the call returns null on any failure instead of throwing. During an outage, turn on circuitBreaker so visitors stop waiting out the timeout.

Release channels

ChannelInstallUse it for
latestnpm i @xenia-martech/sdk-jsProduction
rcnpm i @xenia-martech/sdk-js@rcPreviewing the next release
devnpm i @xenia-martech/sdk-js@devDevelopment builds

Next