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.
emailandnamefill the person's built-in fields. Other keys become custom attributes with string, number or boolean values;nullclears one.- IDs that start with
vh_anon_orvh:anon:are reserved.identify()rejects them and blank IDs withVoidhashInvalidArgumentError.
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
| Situation | Result |
|---|---|
| New user | The anonymous visitor becomes the user. |
| Existing user, such as one from your app | The 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 in | No merge. Events from then on carry the new user. No sign-out is recorded, so call signOut() on sign-out. |
| Same user again | Nothing 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
Something wrong or missing on this page? Tell us by email or on Discord.