VoidhashDocs
DevelopersHelp centerGuidesAPI reference
Dashboard
Get started
Introduction
Quickstart
How Voidhash works
Glossary
Versions and releases
Concepts
Teams, projects and environments
API keys
Products and perks
Offers
Subscription lifecycle
People and identity
Events
Features
Paywalls in your app
Feature flags and A/B tests
Paywall components
Push notifications
Ad attribution in your app
Client SDKs
React Native
Swift
Kotlin
Web
Your backend
Node.js
PHP
Go
Rust
Other languages
Payment sources
Forward store notifications
Developer tools
CLI
MCP server
Query analytics with VoidQL
Web SDK
Web quickstart
Script tag
Identify users
Evaluate feature flags
Track
Track events
Page views and attribution
Privacy
Privacy and consent
Browser storage
Connect a consent tool
Keep data out of analytics
Revenue and attribution
Sell on the web
Stripe revenue
Web-to-app handoff
Delivery
First-party proxy
Tracking domain
Reference
Web SDK reference
Errors
Troubleshooting

Identify users

Link anonymous visitors to your users, so web and app activity belong to one person.

Every visitor starts with an anonymous distinct ID, vh_anon_<uuid>, stored in localStorage. When they sign up or sign in, identify() links that anonymous visitor to your user ID, so their earlier page views, the campaign that brought them and everything after belong to one person. People and identity explains how Voidhash merges people.

Identify after sign-in

Pass your own stable user ID, plus optional traits:

await voidhash.identify("user_123", {
  email: "ada@example.com",
  name: "Ada Lovelace",
});
  • Use an opaque, unique ID, such as a UUID, that your server and mobile app also use. Avoid emails and sequential database IDs.
  • email and name fill the person's built-in fields. Other keys become custom attributes with string, number or boolean values; null clears one.
  • IDs that start with vh_anon_ or vh:anon: are reserved. identify() rejects them and blank IDs with VoidhashInvalidArgumentError.

identify() switches the browser to the user at once and never waits for the network. Events already recorded keep their anonymous ID, feature flags are evaluated again for the user, and React hooks re-render with the new distinctId. Other open tabs of your site follow immediately.

The link itself is stored in the browser and delivered in the background, retried until Voidhash confirms it, across reloads and closed tabs. A browser keeps up to 20 undelivered links. With storeBeforeConsent: false and no consent yet, the link lives in memory for the page only, so identify signed-in users on every page load.

Identify on page load

Identify a signed-in user on every page load, skipping the call when the SDK already uses their ID. This also covers sign-ins on pages without the SDK, such as a hosted sign-in page.

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

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

import { isAnonymousDistinctId } from "@voidhash/web";
import { useVoidhash } from "@voidhash/web/react";
import { useEffect } from "react";

import { useAuth } from "./auth"; // your authentication state

export function useVoidhashIdentity() {
  const { client, distinctId, isInitialized } = useVoidhash();
  const { user, isLoading } = useAuth();

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

Wait until your authentication state has loaded: a session that is still loading is not a signed-out session.

What identify merges

SituationResult
New userThe anonymous visitor becomes the user.
Existing user, such as one from your appThe anonymous visitor is merged into the user; the older person is kept with all page views, campaigns and purchases.
Another user ID while a user is signed inNo merge. Events from then on carry the new user. No sign-out is recorded, so call signOut() on sign-out.
Same user againNothing is merged. New email, name or traits are written; a call without them sends nothing.

A retried 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. See Web-to-app handoff.

Sign out

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

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

signOut() records $sign_out, then starts a new anonymous visitor and session. reset() does the same without the event, for example to clear a guest session. Analytics consent survives both; it belongs to the browser, not the user.

Set person attributes

setPersonAttributes() queues attributes with the events, so they are retried and kept across page loads like them. It needs analytics consent; without it, nothing is stored or sent.

await voidhash.setPersonAttributes({ plan_source: "referral" });

setPersonAttributesSync() writes now and returns the updated person. It needs the network, and without consent it writes nothing and returns the unchanged person:

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

Seed the identity

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

import { createVoidhashClient } from "@voidhash/web";

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

A browser without a stored identity starts as that ID. A browser with another identity is identified as the user during initialize(), as identify() would. An anonymous seed is used only on a browser without a stored identity, as two anonymous visitors cannot be merged.

Next steps

  • Sell on the web
  • Web-to-app handoff
  • Track events

Something wrong or missing on this page? Tell us by email or on Discord.

PreviousScript tagNextEvaluate feature flags

On this page

Identify after sign-inIdentify on page loadWhat identify mergesKeep web and app users the same personSign outSet person attributesSeed the identityNext steps