# Visitor identification

> Why data-user-id matters in StepsKit, how anonymous visitors are handled, and how to identify users after login with stepskit.identify().

*Source: https://stepskit.com/docs/concepts/visitor-identification*

Pass `data-user-id` (or `id` via `setUserAttributes`). It's the single
most important thing you can do for StepsKit, because every
"per-visitor" rule keys on it.

## Why id matters

- **Frequency capping.** "Show once per visitor" requires a stable
  visitor identifier — see
  [Frequency capping](/docs/concepts/frequency-capping.md).
- **Analytics.** Per-user funnel views and tour-completion rates roll up
  by `id`.
- **Per-user targeting.** Targeting a specific person (`email = me@…`)
  during testing needs an `id` to attribute the impression correctly.

## Anonymous visitors

If you don't set an `id`, StepsKit treats the visitor as anonymous:

- Targeting rules that require a specific user attribute will not match.
- Rules that target all visitors (the default) still match.
- "Show once" falls back to a session-scoped check using browser
  storage. It works within a session but resets across devices and after
  clearing site data.

This is a reasonable default for marketing sites or signed-out screens.
It's the wrong default for an authenticated SaaS app — pass `id` as
early as you can.

## Identifying users after login

If you know the user when the page loads (server-rendered), pass
`data-user-id` on the script tag — or any `data-user-*` attribute. The
embed reads them as soon as it boots.

If you only know the user after login (SPA, client-side auth callback,
post-purchase flow), call `stepskit.identify(user)` once you have the
user object. With the default install snippet, the call is queued onto
`window.stepskit._q` before the SDK has finished loading — the queue is
replayed at init, in time for the first tour fetch. No
`Script onLoad` wiring and no `window.stepskit?.identify(...)` null
check needed.

```ts
stepskit.identify({ id: "u_123", email: user.email, plan: user.plan });
```

Behind the scenes, `identify` is an alias for
`setUserAttributes(visitor, { autoRefresh: true })`. If you're still on
the legacy script-tag-only install (no IIFE queue stub), the
`setUserAttributes` form is also fine — the embed coordinates the
in-flight tour fetch internally so a "show once" tour can't play to a
visitor that hasn't been identified yet.

## Choosing an id

Use your internal user ID — whatever value uniquely identifies a user in
your app. It can be a UUID, an integer, a Stripe customer ID, anything
that's stable per user across sessions. Don't use email (changes), and
don't use session IDs (rotate too often).

## Checking it actually worked

On a logged-in page, run this in the browser console:

```js
window.stepskit.validateEnvironment();
```

A non-null `visitorId` and a populated `userAttributes` mean identification
is wired correctly. The `toursFiltered` table in the same output lists every
experience that was held back and the reason — the fastest way to answer
"why isn't my tour showing".

Letting an AI coding agent do the wiring? The
[install prompt](/docs/ai-agents/install-prompt.md) covers this end to end.
