JavaScript API reference

Once the StepsKit script has loaded, it exposes a global window.stepskit object with the methods documented below.

Pre-init queue

Every method below can be called before the SDK script finishes loading. The install snippet creates window.stepskit immediately and installs a shim for each queueable method that pushes the call onto window.stepskit._q. When the embed initializes, it replays the queue in order against the real instance — so identify, setUserAttributes, and friends "just work" without an <Script onLoad> callback or window.stepskit?.method(...) null check.

The only exceptions are the synchronous getters — isPlaying, getTours, and getUserAttributes. These return values immediately, so they require the SDK to be loaded. Calling them pre-init throws a TypeError because the queue stub doesn't define them.

User attributes

identify

identify(visitor: Record<string, string | number | boolean>): Promise<void>

Alias for setUserAttributes(visitor, { autoRefresh: true }). The shape matches Pendo and Intercom-style SDKs, so existing identify patterns translate directly. Safe to call before the SDK script finishes loading — the call is queued and replayed at init, in time for the first tour fetch.

stepskit.identify({ id: "u_123", email: "a@b.com", plan: "pro" });

No ?. null check needed — the install snippet's queue stub captures the call even if the real SDK hasn't downloaded yet.

setUserAttributes

setUserAttributes(
  attrs: Record<string, string | number | boolean>,
  options?: { autoRefresh?: boolean },
): Promise<void>

Merges attrs into the visitor's known attributes. Passing autoRefresh: true re-evaluates visibility against the new attributes (and re-fetches tours when the visitor's id changed) — call this after login or any change that affects targeting. It only does that work when an attribute changed since the page was last evaluated, or the last fetch failed, so calling it on every route change with the same values costs nothing. A tour already on screen stays there unless the visitor is no longer served it. identify is the preferred shape for the post-login case; setUserAttributes is for partial updates without a refresh.

await window.stepskit?.setUserAttributes(
  { id: "user_123", plan: "enterprise" },
  { autoRefresh: true },
);

getUserAttributes

getUserAttributes(): Record<string, string | number | boolean> | undefined

Returns the current attributes, or undefined if none have been set. This is a synchronous getter — it requires the SDK to be loaded, so calling it before the script finishes loading throws.

Tour control

playTour

playTour(tourRef: string): Promise<void>

Starts the named tour immediately, regardless of targeting rules. Useful for "Take a tour" buttons. tourRef is the tour's id or its slug — the same two forms ?stepskit-tour= accepts.

Prefer the id in code you ship. Renaming a tour in the dashboard re-derives its slug from the new name, so a hard-coded slug can stop resolving without anyone touching your integration.

You don't need any JavaScript to trigger a tour from a link. Add a stepskit-tour query parameter with the tour's id or slug to any page URL on your site:

https://app.example.com/dashboard?stepskit-tour=welcome-tour

When the embed loads and sees ?stepskit-tour=, it force-plays that tour immediately — bypassing audience targeting, screen-width, and frequency rules (it plays even if the visitor already saw a show_once tour). The tour must still be published to that domain. Great for "check out this new feature" links in changelogs, emails, or support replies. Copy the parameter from the tour's Settings page and append it to whichever of your own URLs you want people to land on.

Prefer the id over the slug for a link you're about to publish. Renaming a tour in the dashboard re-derives its slug from the new name, which silently breaks every slug link already sitting in a sent email. The id never changes, and the Settings page hands you that form.

A tour still needs a step that belongs on the current page in order to render. If the link lands somewhere else in your app, StepsKit tries to send the visitor to the page the first step is on, filling any wildcards in the pattern from where the visitor currently stands: a tour pinned to /app/*/dashboard resolves to /app/acme/dashboard for someone already standing in the acme workspace. Otherwise the link stays pending and plays as soon as the visitor reaches a matching page on their own.

How far a share link gets depends entirely on the tour's URL pattern.

PatternDoes one link work for everyone?
* (or a manual trigger)Yes. The tour isn't pinned to a page, so any URL on your site works.
A literal path (/settings/billing)Yes. Point the link straight at it.
A wildcard or ^regex patternOnly if the recipient is already inside that part of your app.

That last row is the important one. A link cannot carry a value it doesn't contain: if your app's pages look like /dashboard/project/<their-id>/settings, StepsKit can fill * in from a page the visitor is already on, but it cannot invent an id for someone who lands on /dashboard/projects. For a mailing list where every customer has their own id, don't rely on the link alone — link to your app normally and start the tour yourself once your own router has put them on the right page:

// after your router lands the user on the tour's page
window.stepskit.playTour("9f1c0b7a-4d2e-4a71-9f9d-2b3c4d5e6f70");

Listen for forced_tour_unavailable to find out that a link gave up, and which tour it wanted. This is the hook for routing the visitor yourself:

window.stepskit.on("forced_tour_unavailable", ({ ref, reason }) => {
  // reason: "unknown_tour" | "no_steps" | "unreachable"
  if (reason === "unreachable") {
    // you know this visitor's workspace — send them there, then:
    // window.stepskit.playTour(ref)
  }
});

It fires at most once per link click. Note that a tab simply abandoned with a pending link emits nothing — expiry is evaluated lazily on the next page load.

Lifetime

The ref survives your own redirects: it is stored per tab for 10 minutes, so a link into an authenticated app still works after a bounce through /login. ?stepskit-tour= is removed from the address bar once the tour actually plays, so a refresh or Back doesn't replay it. A value that names no tour of yours is left in the URL as a reload fallback. (If your server redirects and strips the query string before any JavaScript runs, the embed never sees it — put stepskit-tour on a URL your app preserves, or re-append it after login.) Call validateEnvironment() to inspect a pending link: the forcedTour block reports the pattern it expects and the page it would route to.

stopTour

stopTour(): void

Stops the currently playing tour. Counts as a dismissal for frequency capping.

isPlaying

isPlaying(): boolean

Returns true if a tour is currently on screen.

getTours

getTours(): Array<{ id: string; name: string }>

Returns the list of tours currently loaded for this visitor (i.e., the tours that passed targeting and frequency rules at the last fetch).

Re-evaluation

refresh

refresh(): Promise<void>

Re-fetches tours from the StepsKit API with the current user attributes and re-evaluates which one (if any) should play now. You do not need it for client-side route changes: StepsKit patches history.pushState and listens for popstate, so tours, tooltips and banners react to the new page on their own. Call it when the data behind the decision changed — attributes, or a tour published since the page loaded.

setUserAttributes(..., { autoRefresh: true }) and identify refresh for you, but only when an attribute changed since the page was last evaluated (or the last fetch failed). refresh() always re-evaluates, against the server — use it when attributes haven't changed but context has. Neither restarts a tour already on screen: it stays unless the visitor is no longer served it.

Events

on / off

on(event: string, handler: (...args: unknown[]) => void): void
off(event: string, handler: (...args: unknown[]) => void): void

Subscribe and unsubscribe to embed events. Available events include:

  • announcement_clicked — fired when a banner CTA is clicked.
  • forced_tour_unavailable — fired when a ?stepskit-tour= link gave up without playing. The payload is { ref, reason, expectedPattern?, attempts }, where reason is "unknown_tour", "no_steps", or "unreachable".
window.stepskit?.on("announcement_clicked", (payload) => {
  console.log("announcement clicked", payload);
});

Announcements

dismissAnnouncement

dismissAnnouncement(announcementId: string): void

Programmatically dismiss a banner — equivalent to the user clicking the close button.

Teardown

destroy

destroy(): void

Tears down all StepsKit UI (active tours and announcements) and detaches listeners. Useful in single-page apps when the user signs out and you want to fully reset the embed.

Diagnostics

validateEnvironment

validateEnvironment(): EnvironmentReport

Returns a diagnostic snapshot of the embed's current state — handy when a tour you expected to fire isn't firing. The method also logs the filtered-tour list via console.table and the full report via console.log, so the simplest workflow is to open devtools and run:

const report = stepskit.validateEnvironment();

The returned EnvironmentReport includes:

  • apiKey — the masked project API key in use.
  • baseUrl — the API endpoint the embed is targeting.
  • visitorId — the resolved visitor identifier, or undefined if no id was provided.
  • userAttributes — the current attribute bag (or undefined).
  • initialized — whether init() has completed.
  • isPlaying — whether a tour is currently on screen.
  • currentTourId — the ID of the currently-playing tour, or null.
  • toursLoaded / tooltipsLoaded — counts of tours and tooltips fetched.
  • toursFiltered — array of { tourId, name, reason, detail? } for every tour that was loaded but filtered out before playback.
  • warnings — validation messages accumulated since init (e.g. dropped attribute keys from setUserAttributes).

The reason field on toursFiltered is one of:

  • url_pattern_mismatch — the current URL doesn't match the tour's URL rules.
  • screen_width_too_narrow — the viewport is below the tour's minimum width.
  • targeting_failed — the visibility rules didn't match the current visitor's attributes.
  • frequency_capped — show_once already fired for this visitor.
  • inactive — the tour is currently disabled in the dashboard.

This is a pure dev/debug call — safe to invoke any time post-init, but don't ship it in production code paths.

TypeScript

If you installed the stepskit npm package, types ship with it — import the API instead of reaching for the global, and skip the rest of this section:

import stepskit from "stepskit";

On a <script> snippet install there's no import to pull types from, so you need an ambient declaration for the global. This is the same file the React install guide and the AI install prompt hand out, so it stays in sync with the runtime:

// src/stepskit.d.ts
// Only needed for the <script> snippet install. If you installed the "stepskit"
// npm package, delete this file — the package ships its own types.
export {};

type StepsKitAttributes = Record<string, string | number | boolean>;

interface StepsKitToursFilteredEntry {
  tourId: string;
  name: string;
  reason:
    | "url_pattern_mismatch"
    | "screen_width_too_narrow"
    | "targeting_failed"
    | "frequency_capped"
    | "inactive";
  detail?: string;
}

interface StepsKitForcedTourReport {
  ref: string;
  state: "pending" | "played" | "expired";
  attempts: number;
  navigations: number;
  expectedPattern?: string;
  resolvedTarget?: string | null;
}

interface StepsKitEnvironmentReport {
  apiKey: string; // masked (e.g. "sk_live_a3f...***")
  baseUrl: string;
  visitorId: string | undefined;
  userAttributes: StepsKitAttributes | undefined;
  initialized: boolean;
  isPlaying: boolean;
  currentTourId: string | null;
  toursLoaded: number;
  tooltipsLoaded: number;
  toursFiltered: StepsKitToursFilteredEntry[];
  forcedTour?: StepsKitForcedTourReport; // present while a ?stepskit-tour= link is unresolved
  warnings: string[];
}

declare global {
  interface Window {
    stepskit?: {
      identify(visitor: StepsKitAttributes): Promise<void>;
      setUserAttributes(
        attrs: StepsKitAttributes,
        options?: { autoRefresh?: boolean },
      ): Promise<void>;
      getUserAttributes(): StepsKitAttributes | undefined;
      playTour(tourRef: string): Promise<void>;
      stopTour(): void;
      isPlaying(): boolean;
      getTours(): Array<{ id: string; name: string }>;
      refresh(): Promise<void>;
      on(event: string, handler: (...args: unknown[]) => void): void;
      off(event: string, handler: (...args: unknown[]) => void): void;
      dismissAnnouncement(announcementId: string): void;
      destroy(): void;
      validateEnvironment(): StepsKitEnvironmentReport;
      /** No-op today: warns once and discards. Do not wire product events to it. */
      track(event: string, properties?: Record<string, unknown>): void;
    };
  }
}