VoidhashDocs
DocumentationAPI reference
Dashboard
Get started
Introduction
Quickstart
Setup
Setup overview
Create a project
Products and perks
Paywall locations
CLI
Paywall components
Components
Develop a component
Editor panels
Component definition
Publishing
Installing and capabilities
SDKs
Swift
Kotlin
React Native
Web
Store setup
Connect the App Store
Connect Google Play
Connect Stripe
API libraries
Node.js
PHP
Rust
Go
Resources
MCP server
Meta ads
Migrate in observer mode
Delete a person's data
Web SDK
Web quickstart
Identify users
Track events
Page views and attribution
Stripe revenue
Web-to-app handoff
First-party proxy
Feature flags
Troubleshooting

Identify users

Link anonymous visitors to your users, and keep web and app users the same person.

Every visitor starts with an anonymous ID that the SDK creates and stores in localStorage, so page views, campaigns, and events are recorded before anyone signs in. When a visitor signs up or signs in, call identify() with your own user ID. Voidhash then joins the anonymous visitor to that user: their earlier page views, the campaign that first brought them, and anything they do from now on belong to one person.

Identify after sign-in

Pass a stable identifier from your backend, plus optional attributes:

await voidhash.identify(user.id, {
  email: user.email,
  name: user.name,
});

Use an opaque, unique ID such as a UUID, the same one your server and your mobile app use. Do not use an email address or a sequential database ID. IDs that start with vh:anon: are reserved for anonymous visitors.

email and name fill the person's built-in email and name. Other keys become custom attributes. Values can be strings, numbers, or booleans; null clears an attribute.

identify() switches the browser to the user at once and never waits for the network. Events already recorded keep the anonymous ID they were recorded with, events from now on carry the user ID, feature flags are evaluated again for the user, and the React hooks re-render with the new distinctId. It rejects only for an invalid ID: a blank one, or one that starts with vh:anon:.

Identify while offline

The link that joins the anonymous visitor to the user is stored in the browser and delivered in the background. When Voidhash cannot be reached, the browser keeps the new identity and retries the link with growing delays, as soon as the connection comes back or the tab becomes visible again, and on the next page load. It survives reloads and closed tabs. flushAnalytics() delivers it at once when it can.

Identify on page load

For a signed-in user, identify on every page load, skipping the call when the SDK already uses their ID:

if (voidhash.getDistinctId() !== user.id) {
  await voidhash.identify(user.id, { email: user.email });
}

That covers a user who signs in on a page where the SDK is not loaded, such as a hosted sign-in page.

In React, follow your authentication state with useVoidhash().distinctId, including sign-out:

const { client, distinctId, isInitialized } = useVoidhash();
const { user, isLoading } = useAuth(); // your authentication state

useEffect(() => {
  if (!isInitialized || isLoading) return;
  if (user && distinctId !== user.id) {
    void client.identify(user.id, { email: user.email });
  } else if (!user && distinctId && !distinctId.startsWith("vh:anon:")) {
    void client.signOut();
  }
}, [client, distinctId, isInitialized, isLoading, user]);

Wait until your authentication state has loaded, as isLoading does above. A session that is still loading is not a signed-out session.

What identify merges

  • A new user. The anonymous visitor becomes the user. Nothing else changes.
  • An existing user, for example one who signed up in your app first. The anonymous visitor is merged into that user, and the older of the two people is kept. Their page views, campaign traits, purchases, and subscriptions end up on one person.
  • Switching accounts. Calling identify() with another user ID while a user is signed in does not merge the two users, the same as in the mobile SDKs. Events from then on carry the new user, who starts from their own history and gets the email, name, and attributes you pass. The switch records no sign-out, so call signOut() when a user signs out, for example on a shared computer.
  • The same user again. Nothing is merged. New email, name, or attributes are written to the user; a call without them sends nothing.

A retried or repeated link changes nothing the second time.

Keep web and app users the same person

Call identify() with the same user ID on the web and in your iOS, Android, or React Native app. The order does not matter: whichever side identifies first creates the user, and the other side's anonymous visitor is merged into it. Revenue from Stripe, the App Store, and Google Play then lands on one person, credited to the campaign that first brought them.

See Web-to-app handoff for funnels where the visitor pays on the web and continues in the app.

Sign out

Sign out of Voidhash after your own sign-out completes:

await auth.signOut();
await voidhash.signOut();

signOut() records a $sign_out event for the user, then starts a new anonymous visitor and a new session. Events recorded before keep the user's ID. Use reset() to do the same without recording the event, for example when you clear a guest session.

Other open tabs of your site follow an identify, sign-out, or reset at once, so a tab left open never keeps recording under a user who signed out in another one.

Analytics consent is kept across sign-out and reset; it belongs to the browser, not the user.

Set person attributes

Set attributes on the current person without waiting for the server:

await voidhash.setPersonAttributes({
  name: "Ada Lovelace",
  plan_source: "referral",
});

The update travels with the events, so it is queued, retried, and kept across page loads like them. It needs analytics consent; without it, nothing is stored or sent.

To write the attributes now and read the updated person back, use setPersonAttributesSync(), which needs the network:

const person = await voidhash.setPersonAttributesSync({ plan_source: "referral" });

Without analytics consent, it writes nothing and returns the unchanged person.

Read the distinct ID

getDistinctId() returns the current ID, anonymous or identified:

const distinctId = voidhash.getDistinctId();

Send it to your server when it creates a Stripe Checkout Session or customer, so the purchase is attributed to this visitor. See Stripe revenue. It is also useful in support logs.

Seed the identity

The distinctId client option starts the client with an ID you already know:

const voidhash = createVoidhashClient({
  distinctId: user.id,
  publishableKey: "vh_pk_…",
});

On a browser without a stored identity, the client starts as that ID. When the browser already has another identity, initialize() identifies the user, as identify() would, without waiting for the network. An anonymous ID seed is used only on a browser without a stored identity; two anonymous visitors cannot be merged.

Next steps

  • Attribute Stripe revenue to the visitor
  • Hand visitors over to your app
  • Track events
PreviousWeb quickstartNextTrack events

On this page

Identify after sign-inIdentify while offlineIdentify on page loadWhat identify mergesKeep web and app users the same personSign outSet person attributesRead the distinct IDSeed the identityNext steps