## How it works 1. You create a workspace in the [dashboard](https://app.justonecx.qd.je) and get a **public key**. 2. You add the SDK to your site with that key. 3. You build tours, announcements, surveys and chat in the dashboard. 4. The SDK fetches what is published for the current visitor and shows it. The public key is not a secret. It only grants access to content you have already published. See [Security](/security). --- # Quickstart Get a tour showing on your site in about five minutes. ## 1. Get your public key 1. Sign in at [app.justonecx.qd.je](https://app.justonecx.qd.je) and open your workspace. 2. Go to **Settings → Developers**. 3. Copy the **public key**. It starts with `pk_`. ## 2. Allow your site In **Settings → Developers → Domains & CORS**, add the origin of your site, for example `https://www.example.com`. For local work, add `http://localhost:3000` (or your dev port). See [Allowed origins](/guides/allowed-origins). ## 3. Add the SDK Paste this before the closing `` tag of every page, with your own key: ```html ``` The script starts itself. There is nothing else to write. Using React, Vue, Angular or another framework? See [Frameworks](/install/frameworks). Prefer npm? See [npm package](/install/npm). ## 4. Publish something 1. In the dashboard open **Tours** and create a tour. 2. Add a step and pick the element it should point at. 3. Set the tour to **Published**. ## 5. Check it works Open your site in a new browser window. The tour starts. If it does not, open [Troubleshooting](/troubleshooting). ## 6. Tell us who the user is (recommended) After a user signs in, call `identify` so you can target by plan or role: ```js window.JustOneCX.identify("user-42", { plan: "growth", role: "admin" }); ``` When the user signs out, call `reset`: ```js window.JustOneCX.reset(); ``` Before you go live, turn on [identity verification](/guides/identity-verification). ## Next steps - [API reference](/api/) - [Targeting and segments](/guides/targeting) - [Set it up with your AI tool](/ai/) --- # Build with AI You can ask your AI coding tool to add JustOneCX for you. You say what you want. It reads our docs and edits your project. Example prompts: - "Add JustOneCX to this app. My public key is `pk_...`." - "Identify the user after login and reset on logout." - "Track a `completed_checkout` event when an order succeeds." - "Show a survey after the user finishes onboarding." There are three ways to give your AI the knowledge. Start with the first. | Way | Best for | Setup | | --- | --- | --- | | [`llms.txt`](#llms-txt) | Any tool, no install | Paste a URL | | [Agent skill](/ai/skill) | Claude Code, Cursor, Codex, Copilot | Copy one file | | [MCP server](/ai/mcp) | Tools with MCP support | One config entry | ## llms.txt {#llms-txt} We publish our docs in a format made for AI tools: - [`/llms.txt`](https://docs.justonecx.qd.je/llms.txt): an index of every page. - [`/llms-full.txt`](https://docs.justonecx.qd.je/llms-full.txt): the full docs in one file. - Any page as Markdown: add `.md` to its URL, for example `/quickstart.md`. Tell your AI: "Read https://docs.justonecx.qd.je/llms-full.txt and add JustOneCX to my app. My public key is `pk_...`." ## What you still do yourself - Get the public key from **Settings → Developers**. - Add your site's origin to **Domains & CORS**. - Review the change. Do not give your AI the identity secret. It belongs on your server only. --- # MCP server `@justonecx/mcp` lets MCP-capable tools look up our docs and generate correct code for your framework. It needs no account and reads only public documentation. ::: warning Not published yet The package is in the repository (`packages/mcp`) and is not on npm yet. Until it is, use the [agent skill](/ai/skill) or [`llms.txt`](/ai/#llms-txt). ::: ## Add it Add this to your tool's MCP configuration: ```json { "mcpServers": { "justonecx": { "command": "npx", "args": ["-y", "@justonecx/mcp"] } } } ``` For Claude Code: `claude mcp add justonecx -- npx -y @justonecx/mcp` ## Tools | Tool | What it does | | --- | --- | | `search_docs` | Searches the docs and returns matching sections. | | `get_install_snippet` | Returns install code for a framework. Pass your public key and API URL. | | `get_api_reference` | Returns the reference for `init`, `identify`, `group`, `track`, `reset` or `startTour`. | | `generate_identify_code` | Returns client and server code for identify with identity verification. | All tools return text only. They never read or change your files. Your AI tool does that. --- # Agent skill The skill tells an AI coding agent exactly how to add JustOneCX to your project: how to find your framework, where to call `init`, where to call `identify`, and what to avoid. ## Install Download the skill file and put it where your tool reads instructions: | Tool | Location | | --- | --- | | Claude Code | `.claude/skills/justonecx-integration/SKILL.md` | | Cursor | `.cursor/rules/justonecx.mdc` | | Codex, Copilot, others | Append to `AGENTS.md` in your repo root | ```sh # Claude Code mkdir -p .claude/skills/justonecx-integration curl -o .claude/skills/justonecx-integration/SKILL.md https://docs.justonecx.qd.je/ai/justonecx-integration/SKILL.md # Any tool that reads AGENTS.md curl https://docs.justonecx.qd.je/ai/justonecx-integration/SKILL.md >> AGENTS.md ``` The file is plain Markdown. You can read it before you install it: [SKILL.md](https://docs.justonecx.qd.je/ai/justonecx-integration/SKILL.md). ## Use Tell your agent what you want: > Add JustOneCX to this app with public key `pk_...`. Identify the signed-in user and reset on sign-out. --- # identify, group, track Run these after `init()`. ## identify ```ts identify("user-42", { role: "admin", plan: "growth" }); ``` Tells JustOneCX who the current user is. Traits **merge** into what the user already has. Call it on every page load to keep traits fresh. ```ts function identify(userId: string, traits?: Traits, options?: { userHash?: string }): Promise; ``` The promise resolves when the backend has saved the traits. It never rejects. Await it, then call `init()` again, if you need tours that depend on the new traits to appear straight away. `options.userHash` is the HMAC signature of `userId` from your server. See [Identity verification](/guides/identity-verification). ::: warning `identify()` alone is not proof of identity. The public key is visible in your page source, so any page that has it can claim any `userId`. Turn on identity verification before you rely on identity for anything sensitive. ::: ## group {#group} ```ts group("acme-inc", { plan: "growth", seats: 12 }); ``` Sets the account (company, team, organisation) the user belongs to. ```ts function group(accountId: string, traits?: Traits, options?: { accountHash?: string }): Promise; ``` `accountHash` is the HMAC signature of `accountId`, computed the same way as `userHash`. ## track {#track} ```ts track("clicked_upgrade_cta", { plan: "growth" }); ``` Records a custom event. It is tied to whatever `identify` and `group` calls came before it. ```ts function track(eventName: string, properties?: Traits): void; ``` - Events sent before `identify` are still recorded. They have no user attached, as for any anonymous visitor. - A checklist task can wait for an event name. When `track()` sends that exact name, the task completes. - A survey can use an event name as its trigger. The next matching `track()` call shows the survey once. ## Same person, many devices The visitor id is per browser. If the same user calls `identify("user-42")` from two devices, JustOneCX links both to the same person for analytics and targeting. **Chat is the exception.** A conversation stays with the browser that started it, so a claimed identity cannot read another person's chat history. --- # API reference The SDK exports these functions. In the [script tag](/install/script-tag) build they live on `window.JustOneCX`. | Function | Purpose | | --- | --- | | [`init(options)`](/api/init) | Start the SDK. Fetches and shows published content. | | [`identify(userId, traits?, options?)`](/api/identify) | Set who the current user is. | | [`group(accountId, traits?, options?)`](/api/identify#group) | Set the account the user belongs to. | | [`track(eventName, properties?)`](/api/identify#track) | Record a custom event. | | [`reset(options?)`](/api/reset) | Clear identity on sign-out. | | [`startTour(tourId)`](/api/reset#starttour) | Replay a tour on demand. | ## Rules for all calls - Run `init()` first. `identify`, `group`, `track` and `startTour` called before `init()` log a console error and do nothing. They never throw. - Every call is best effort. A failed network request never throws and never blocks your page. - Traits are plain objects of strings, numbers and booleans. ## Types ```ts interface InitOptions { publicKey: string; apiUrl?: string; disableChat?: boolean; trackRouteChanges?: boolean; } ``` `Traits` is a flat key/value object. Import it from the package: `import type { Traits } from "@justonecx/sdk"`. --- # init ```ts import { init } from "@justonecx/sdk"; await init({ publicKey: "pk_your_workspace_key", apiUrl: "https://api.justonecx.qd.je", trackRouteChanges: true, }); ``` `init` starts the SDK. It creates an anonymous visitor id (stored in the browser's `localStorage`), fetches the content published for this visitor, and shows it. Tours, announcements and surveys load independently, so a failure in one never blocks the others. It also mounts the chat bubble. ## Options | Option | Type | Default | Description | | --- | --- | --- | --- | | `publicKey` | `string` | none (required) | Workspace public key from **Settings → Developers**. | | `apiUrl` | `string` | `http://localhost:8000` | Backend origin. Always set it on real sites. The SDK logs a warning if you do not. | | `trackRouteChanges` | `boolean` | `false` | Re-run `init()` on every client-side navigation. Use it in single-page apps. Leave it off if you already call `init()` from your router. | | `disableChat` | `boolean` | `false` | Do not mount the chat bubble. Use it when you run the SDK for your own staff, so staff do not open support chats in their own product. | ## Script tag equivalents | Attribute | Option | | --- | --- | | `data-public-key` | `publicKey` | | `data-api-url` | `apiUrl` | | `data-track-route-changes="true"` | `trackRouteChanges: true` | ## Calling init again Calling `init()` again is safe. A tour or announcement already on screen is not fetched and shown a second time. This is what `trackRouteChanges` does for you. ## Server-side rendering Do not call `init()` on the server. It uses `window` and `document`. Importing the package on the server is safe. --- # reset and startTour ## reset Call `reset()` when the user signs out. ```ts import { reset } from "@justonecx/sdk"; await reset(); // clear identity, start a new anonymous visitor, run init() again await reset({ remount: false }); // clear only, do not run init() again ``` Why it matters: the visitor id is a permanent id in `localStorage`. On a shared browser, the next person who signs in would otherwise inherit the previous person's open chat and other visitor state. `reset()` does these steps: 1. Removes the chat bubble and closes its connection. 2. Stops any tour that is still playing and removes launcher buttons. 3. Deletes the stored visitor id and creates a new one. 4. Runs `init()` again with the same key and URL, unless `remount` is `false` or `init()` never ran. A new chat bubble appears after `reset()`. This is by design. ## startTour {#starttour} Replay a tour from your own button, for example a "Show me around" item in a help menu. ```ts import { startTour } from "@justonecx/sdk"; startTour("tour-id"); ``` `startTour` plays the tour immediately. It ignores the tour's targeting, frequency cap and completion state. The tour id is in the URL of the tour's edit page in the dashboard. A tour can also show its own **persistent launcher** button. Enable it in the tour editor, on the **Launcher** tab. The launcher calls `startTour` for you. --- # Allowed origins Add each site that runs the SDK in **Settings → Developers → Domains & CORS**. Use the full origin: scheme, host and port when needed. ```text https://www.example.com https://app.example.com http://localhost:3000 ``` If you forget a site, the browser blocks the SDK's requests and shows a CORS error in the console. ::: warning The allow-list is not authentication It only restricts browsers. A script outside a browser can send any `Origin` header. Treat the public key as public, and use [identity verification](/guides/identity-verification) to protect identities. ::: --- # Announcements An announcement is a message you show in your product: a new feature, a maintenance notice, a promotion. It appears as a modal or a banner. ## Create an announcement 1. Open **Announcements** in the dashboard. 2. Write the content and choose modal or banner. 3. Choose who sees it: everyone, or a [segment](/guides/targeting). 4. Publish. ## Behaviour - A banner is pinned to the top of the page and needs no target element. - A visitor who dismisses an announcement does not see it again. - Announcements and tours load independently. A problem with one never blocks the other. - An announcement can have a call-to-action button that opens a link or starts a tour. ## Public changelog Announcements can also feed a public changelog page. See your workspace settings for the link. ## Links Only `http` and `https` links are allowed in announcement content. Other link types are removed. --- # Live chat `init()` mounts a chat bubble in the bottom-right corner. You need no extra code. ## How it works 1. A visitor clicks the bubble. The SDK creates or resumes their open conversation. 2. Messages arrive live over a WebSocket. If the socket does not connect within about three seconds, the widget polls every four seconds while the panel is open. 3. Sending a message always uses a normal HTTP request. 4. Your team replies from the **Inbox** in the dashboard. Outside business hours, the visitor sees your configured offline message. It is shown only in the widget and is not saved as a message. ## Unread badge The bubble shows a count of agent messages that arrived while the panel was closed. It resets when the panel opens. ## Hide chat for some users Pass `disableChat: true` to `init()`. Use it for your own staff. See [init](/api/init). ## Several widgets in one corner Chat, tour launchers, surveys and checklists can share a corner. The SDK stacks them so they do not overlap. You can choose which one sits closest to the corner in **Workspace Settings → Widget priority**. ## Privacy A conversation belongs to the browser that started it. Calling `identify()` does not give another browser access to it. Call [`reset()`](/api/reset) on sign-out so the next person on a shared browser starts fresh. --- # Identity verification Without verification, any page that has your public key can call `identify("someone-else")` and write to that person's profile. Identity verification stops this. Your server signs each user id, and JustOneCX rejects calls without a valid signature. ## Turn it on 1. Open **Settings → Developers → Identity verification**. 2. Enable it and copy the **identity secret**. 3. Store the secret on your server only. Never put it in client code. ## Sign on your server ```js // Node.js, server only import { createHmac } from "node:crypto"; const userHash = createHmac("sha256", process.env.JUSTONECX_IDENTITY_SECRET) .update(user.id) .digest("hex"); ``` Send `userHash` to the browser with the signed-in user, for example in your session payload. ## Pass it to the SDK ```ts identify(user.id, { plan: user.plan }, { userHash }); group(account.id, { plan: account.plan }, { accountHash }); ``` `accountHash` is the HMAC of the account id, computed the same way. ## What happens without a valid hash When verification is on, a call without a valid hash returns normally but is ignored. It cannot merge into another person's profile or change their traits. ## Rotating the secret Rotating the secret invalidates every hash you issued. Deploy the new secret to your server first, then rotate in the dashboard. --- # Surveys and NPS Ask visitors how it is going. Surveys support NPS, CSAT, CES and free-text feedback, and can use the block builder for custom layouts. ## Create a survey 1. Open **Surveys** in the dashboard. 2. Pick a type and write the question. 3. Choose where it appears: slide-out, modal or banner. 4. Choose when it appears: on page load, after scrolling, or when your code sends a specific event. 5. Set repeat frequency, then publish. ## Trigger a survey from your code Set the survey trigger to **event** and enter an event name. Then call: ```ts import { track } from "@justonecx/sdk"; track("completed_onboarding"); ``` The next `track()` call with that exact name shows the survey once. ## Follow-ups and branching You can show different follow-up questions by score band, for example a different prompt for detractors and promoters. ## Test mode Use the **Test survey** mode in the builder to see the survey without saving a response. ## Responses Responses appear in the dashboard under the survey. A survey type cannot change after its first response. --- # Targeting and segments Targeting decides who sees a tour, announcement or survey. The backend applies it before content reaches the browser, so hidden content is never sent. ## Modes | Mode | Tours | Announcements | | --- | --- | --- | | Everyone | Yes | Yes | | Page (URL rules) | Yes | No | | Lifecycle (new or returning) | Yes | No | | Attribute | Yes | No | | Segment | Yes | Yes | Page rules are checked in the browser, because the backend does not know the current page. ## Where the data comes from - [`identify`](/api/identify) sets user traits such as `plan` and `role`. - [`group`](/api/identify#group) sets account traits. - [`track`](/api/identify#track) records events. An anonymous visitor who never calls these still matches Everyone, Page and Lifecycle rules. Attribute and segment rules need trait data. An account-level segment never matches a visitor who has not called `group()`. ## Create a segment 1. Open **Segments** in the dashboard. 2. Define conditions on the traits you send with `identify` and `group`. 3. Use the segment in a tour, announcement or survey. ## Keep traits fresh Traits merge, so call `identify` and `group` on every page load. If you need targeted content to show right after a trait changes, `await identify(...)` and then call `init()` again. --- # Product tours A tour is a series of steps that point at elements on your page, or show a modal or banner. Visitors move through it with Next and Back. ## Create a tour 1. In the dashboard, open **Tours** and create one. 2. Add steps. For a tooltip step, pick the element it points at. 3. Set targeting (who sees it, on which pages). 4. Publish. The SDK fetches published tours on `init()` and plays the ones that match the visitor. ## How steps are shown - A tooltip step highlights the element found by the step's selector. - If a step's element is not on the page, the SDK skips that step and logs a console warning. - If no step in a tour can be found, the SDK skips the whole tour. It does not mark it as seen, because the visitor never saw it. - Finishing or dismissing a tour marks it as seen. It is not shown again automatically. ## Replay and launcher Enable the **persistent launcher** on the tour's **Launcher** tab to show a "Replay tour" button. It stays available after the visitor finishes. You can set its screen corner. A visitor can dismiss it for good in their browser. You can also build your own button with [`startTour`](/api/reset#starttour). The tour's **frequency cap** limits only automatic playback. A manual replay always works. ## Checklists A checklist step shows tasks. A visitor completes a task by clicking it. You can instead bind a task to an event name. When your code calls `track("that_event")`, the task completes. Whichever happens first wins. ## Single-page apps Set `trackRouteChanges: true` so page-based targeting re-runs on each navigation. See [init](/api/init). ## Preview on your site The tour builder has a **Preview on your site** action. It uses the JustOneCX browser extension to play your draft on your own live page. --- # Frameworks Every framework uses the same calls. Three rules apply everywhere: 1. Call `init()` once, in the browser only, after the app mounts. 2. In single-page apps, pass `trackRouteChanges: true`. The SDK then re-runs itself on each client-side navigation, so page targeting stays correct. 3. Call `identify()` after sign-in and `reset()` on sign-out. Use these options in all examples: ```ts const options = { publicKey: "pk_your_workspace_key", apiUrl: "https://api.justonecx.qd.je", trackRouteChanges: true, }; ``` ::: info The examples import `@justonecx/sdk`. If you have not installed it, use the [script tag](/install/script-tag) instead. ::: ## React, Next.js, Remix, Preact ```tsx "use client"; // Next.js App Router only import { useEffect } from "react"; import { identify, init } from "@justonecx/sdk"; export function JustOneCX({ user }: { user?: { id: string; plan: string } }) { useEffect(() => void init(options), []); useEffect(() => { if (user) void identify(user.id, { plan: user.plan }); }, [user]); return null; } ``` Render `` once in your root layout. ## Vue and Nuxt ```ts // Vue: main.ts, after app.mount() import { init } from "@justonecx/sdk"; void init(options); ``` ```ts // Nuxt: plugins/justonecx.client.ts // The ".client" suffix keeps it out of server rendering. import { init } from "@justonecx/sdk"; export default defineNuxtPlugin(() => { void init(options); }); ``` ## Angular ```ts import { isPlatformBrowser } from "@angular/common"; import { Component, PLATFORM_ID, inject } from "@angular/core"; import { init } from "@justonecx/sdk"; @Component({ selector: "app-root", templateUrl: "./app.html" }) export class App { constructor() { if (isPlatformBrowser(inject(PLATFORM_ID))) void init(options); } } ``` ## Svelte and SvelteKit ```svelte ``` Put this in `+layout.svelte` (SvelteKit) or `App.svelte`. ## Solid and SolidStart ```tsx import { onMount } from "solid-js"; import { init } from "@justonecx/sdk"; onMount(() => void init(options)); ``` ## Astro, Lit, Alpine, jQuery, plain TypeScript Call `init(options)` from any client-side script. In Astro, put it in a ` ``` Place it before ``. The script reads its own `data-*` attributes and starts by itself. ## Attributes | Attribute | Required | Description | | --- | --- | --- | | `data-public-key` | Yes | Your workspace public key. | | `data-api-url` | Yes on real sites | Backend origin. Without it the SDK uses `http://localhost:8000`, which only works for local development. | | `data-track-route-changes` | No | Set to `"true"` on sites that change pages without a full reload. | ## Pin a version The URL above always serves the latest SDK and is cached for five minutes. To avoid surprises, pin a version. A pinned file never changes: ```html ``` See [SDK versions](/versions). ## Calling the API The script adds `window.JustOneCX`: ```js window.JustOneCX.identify("user-42", { plan: "growth" }); window.JustOneCX.track("clicked_upgrade"); window.JustOneCX.reset(); ``` Calls made before the script has loaded fail. Place your own code after the script tag, or run it after the `load` event. ## Platform notes - **WordPress**: add the snippet with a header/footer plugin, or in your theme's `footer.php` before `wp_footer()`. - **Webflow**: Project settings → Custom code → Footer code. - **Shopify**: edit `theme.liquid` and paste the snippet before ``. - **Google Tag Manager**: use a Custom HTML tag that fires on all pages. Add `data-track-route-changes="true"` if the site is a single-page app. --- # Public developer docs VitePress site served at `https://docs.justonecx.qd.je`. Audience: developers adding the SDK to their product. This is **not** the internal `docs/` site (QA, architecture, database) and nothing internal belongs here. ```sh pnpm --filter public-docs dev # http://localhost:8082 pnpm --filter public-docs build # vitepress build + scripts/postbuild.mjs pnpm --filter public-docs test # checks code samples against the SDK's real exports ``` ## What the build emits `scripts/postbuild.mjs` adds AI-friendly files to `.vitepress/dist` next to the HTML: - `/llms.txt`: index of every page. - `/llms-full.txt`: every page in one file. `packages/mcp` reads this file for `search_docs`. - `/.md`: raw Markdown per page. - `/ai/justonecx-integration/SKILL.md`: agent skill, copied from `public/ai/`. ## Writing rules - Write for someone who has never seen the product. Short pages, one task each, copy-paste code. - Use the placeholder key `pk_your_workspace_key` and the real URLs `https://api.justonecx.qd.je` and `https://app.justonecx.qd.je/sdk/justonecx-sdk.global.js`. - `pnpm test` fails if a sample imports a name `@justonecx/sdk` does not export, or calls `window.JustOneCX.` for a function the script-tag bundle does not expose. Keep samples real. - New page: add the file, add it to the sidebar in `.vitepress/config.ts`. - When the SDK version changes, update `versions.md` and the pinned URL in `install/script-tag.md`. - When `@justonecx/sdk` is published to npm, remove the "not published yet" warnings in `install/npm.md` and `public/ai/justonecx-integration/SKILL.md`. Same for `@justonecx/mcp` in `ai/mcp.md`. ## Deploy `scripts/vps/deploy.sh` builds this app and rsyncs it to `/var/www/justonecx-docs`. Use `deploy.sh --docs-only` for a docs-only release. Caddy config lives in `scripts/vps/Caddyfile`. --- # Security ## The public key is public The key that starts with `pk_` is like a publishable key. It is safe in page source. It identifies your workspace and only grants access to content you have published. It does not need to change when you add or edit content. Rotate it from **Settings → Developers** only if you want to retire an embed. ## Protect identities Anyone with your public key can call `identify` with any user id. Turn on [identity verification](/guides/identity-verification) and sign user ids on your server. Never put the identity secret in client code. ## Allowed origins The [allow-list](/guides/allowed-origins) restricts which browsers can use your key. It is not authentication. ## Content Security Policy If your site sends a Content-Security-Policy header, allow: | Directive | Value | | --- | --- | | `script-src` | `https://app.justonecx.qd.je` (script tag install only) | | `connect-src` | `https://api.justonecx.qd.je` and `wss://api.justonecx.qd.je` (chat) | | `style-src` | `'unsafe-inline'`, because the SDK injects one `