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> | undefinedReturns 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.
Play by link (?stepskit-tour=)
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-tourWhen 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.
What a link can and can't reach
How far a share link gets depends entirely on the tour's URL pattern.
| Pattern | Does 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 pattern | Only 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");Knowing when a link didn't land
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(): voidStops the currently playing tour. Counts as a dismissal for frequency capping.
isPlaying
isPlaying(): booleanReturns 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): voidSubscribe 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 }, wherereasonis"unknown_tour","no_steps", or"unreachable".
window.stepskit?.on("announcement_clicked", (payload) => {
console.log("announcement clicked", payload);
});Announcements
dismissAnnouncement
dismissAnnouncement(announcementId: string): voidProgrammatically dismiss a banner — equivalent to the user clicking the close button.
Teardown
destroy
destroy(): voidTears 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(): EnvironmentReportReturns 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, orundefinedif noidwas provided.userAttributes— the current attribute bag (orundefined).initialized— whetherinit()has completed.isPlaying— whether a tour is currently on screen.currentTourId— the ID of the currently-playing tour, ornull.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 fromsetUserAttributes).
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_oncealready 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;
};
}
}