Web SDK
Configure @voidhash/web and look up its options, methods, React bindings, and events.
The web SDK, @voidhash/web, brings Voidhash analytics and attribution to websites and web apps:
page views, product events, sessions, the campaign or ad that brought each visitor, and the person
behind them. Purchases on the web go through Stripe, and Voidhash records them from Stripe itself;
the SDK supplies the visitor's id so each purchase lands on the right person and campaign. A visitor
who signs in on your website and in your iOS, Android, or React Native app is one person in
Voidhash.
Use this page to create the client, set its options, and find the method, hook, or event you need. Each feature has its own guide, linked from the section that introduces it. To get started, follow the quickstart.
Compatibility
| Requirement | Version |
|---|---|
| Browsers | Current versions of Chrome, Edge, Firefox, and Safari. The package targets ES2022. |
effect | Peer ^4.0.3. Install 4.0.3 or a later 4.x release. |
react | Optional peer, only for @voidhash/web/react. React 18 or later. |
| Frameworks | Any. Next.js, React Router, TanStack Router, Vue, Svelte, and plain HTML pages all work. |
| Rendering | Server rendering is safe: creating the client and rendering the provider never touch the DOM. |
| Modules | ES modules for browsers and bundlers. The CommonJS build needs Node 20.19 or 22.12 or later, because effect is an ES module, and its two entry points do not share classes, so instanceof does not hold across them. |
The SDK runs in the browser. Call initialize() only in the browser; on a server it rejects with an
error that says it needs a browser. To send
events from your own backend, use the Event Capture API or the
Node.js library.
Data collection
Account for the data the SDK sends to Voidhash in your privacy policy and cookie banner.
| Data | When the SDK collects it | Used for |
|---|---|---|
| Anonymous ID | Always. The SDK creates one per browser and stores it in localStorage. | Identity, analytics |
| User ID | When you call identify() with your own user ID. | Identity, analytics |
| Email address and name | Only when you pass them to identify() or set them as person attributes. | Identity |
| Pages and product events | While analytics consent is granted, the default. The page's origin, path, and title, the referrer's origin and path, and the events you track. | Analytics |
| Campaign and ad clicks | While analytics consent is granted. utm_* parameters, ad IDs, and Meta's _fbc and _fbp cookies. | Attribution |
| Device details | With every request: the browser user agent, language, and platform. With events, while consent is granted: time zone and screen and viewport size. | Analytics |
| Coarse location | While analytics consent is granted. Voidhash derives a city-level location from the request's address. | Analytics |
The SDK never records query strings other than the campaign parameters, never sets cookies, and
does not share data with other companies. It reads Meta's cookies only when your site already sets
them. It stores its identifiers, queue, and settings in localStorage, and a campaign seen before
consent in sessionStorage; many consent regimes treat that storage like cookies, so ask for consent
before analytics where your visitors require it. See Analytics consent.
Create the client
Create one client per page and keep it for the lifetime of the page.
import { createVoidhashClient } from "@voidhash/web";
export const voidhash = createVoidhashClient({
publishableKey: "vh_pk_…",
});The publishable key is safe to ship in a web page. Never put a vh_sk_… secret key in browser code.
initialize() loads the SDK's saved state, records the first page view, and starts sending events
in the background. It resolves without waiting for the network, so a slow or unreachable Voidhash
never delays your page. Calls made while it runs wait for it, so an event tracked during page load
is kept; calls made before you call initialize() reject with VoidhashNotInitializedError. In
React, the provider initializes the client for you.
await voidhash.initialize();Create one client per project on a page. Two clients for the same project each record page views, so every page would be counted twice.
Client options
| Option | Default | Description |
|---|---|---|
publishableKey | none | Required. Your project's vh_pk_… key. |
baseUrl | https://api.voidhash.com | API origin. Point it at a first-party proxy or a self-hosted deployment. |
analyticsConsent | "granted" | Consent to start with: "granted", "denied", or "pending". A choice stored by setAnalyticsConsent() wins. |
distinctId | Stored or new anonymous ID | Seeds the identity. Usually omit it and call identify(). |
onDiagnostic | none | Called for every SDK diagnostic. See Troubleshooting. |
analytics | See below | Page view and event delivery options. |
featureFlags | See below | Feature flag caching options. |
observerMode | false | Has no effect in the browser. |
analytics takes these fields.
| Field | Default | Description |
|---|---|---|
capturePageViews | true | Records $pageview on load and after every client-side navigation. Attribution is recorded either way. |
enabled | true | false turns analytics off, as if consent were denied, without storing a choice. |
baseUrl | Client's baseUrl | Origin for analytics only, when it differs from the API origin. |
flushIntervalMs | 5000 | How often queued events are sent. |
maxBatchSize | 20 | Events per request. Reaching it sends the batch at once. |
maxBatchBytes | 262144 | Upper bound of one request body, in bytes. |
maxQueueSize | 1000 | Events kept while offline. The oldest are dropped first. |
featureFlags takes these fields.
| Field | Default | Description |
|---|---|---|
ttlMs | 300000 | Age after which cached flags are refreshed in the background. |
persist | true | Keeps flags in localStorage so the next page load starts with them. |
prefetchOnInit | false | Evaluates every flag during initialize(), in the background. |
refreshOnOnline | true | Refreshes flags when the browser reconnects, at most once a minute. |
refreshOnVisibility | true | Refreshes flags when the tab becomes visible, at most once a minute. |
createVoidhashClient throws a VoidhashConfigurationError for a missing publishableKey, a
secret vh_sk_… key, a URL that is not an absolute http:// or https:// URL, a consent value
other than the three above, or a size or interval that is not a positive integer. Blank strings
count as not set.
Client methods
Methods that return a promise wait for a running initialize(). They reject with
VoidhashNotInitializedError when it was never called and with VoidhashDestroyedError after
destroy(). Network failures reject only setPersonAttributesSync(), which writes to Voidhash
directly. Everything else waits for the network on its own: events and identity links stay in the
queue until Voidhash accepts them, and reads answer from the cache.
| Method | Returns | Description |
|---|---|---|
initialize() | Promise<void> | Loads saved state and starts the SDK. Safe to call more than once. |
destroy() | Promise<void> | Sends what it can, removes listeners, and stops the client for good. |
track(name, properties?, options?) | Promise<void> | Queues a product event; never waits for the network. See Track events. |
page(name?, properties?, options?) | Promise<void> | Records a $pageview yourself. See Page views. |
identify(userId, traits?) | Promise<void> | Switches to your user ID and links the visitor in the background. See Identify users. |
signOut() | Promise<void> | Records $sign_out, then starts a new anonymous identity and session. |
reset() | Promise<void> | Starts a new anonymous identity and session without recording a sign-out. |
setPersonAttributes(attributes) | Promise<void> | Queues person attributes, delivered with the events. |
setPersonAttributesSync(attributes) | Promise<VoidhashPerson> | Writes person attributes now and returns the person, { personId, distinctId, email, name }. Rejects when Voidhash cannot be reached. |
getDistinctId() | string | undefined | The current anonymous or identified ID. Pass it to Stripe; see Stripe revenue. |
getStripeClientReferenceId() | string | undefined | The distinct ID in the form Stripe Payment Links and the pricing table accept; undefined for a user ID they would drop. See Stripe revenue. |
getSessionId() | string | undefined | The session the next event joins. undefined without consent or after 30 idle minutes. |
setAnalyticsConsent(consent) | Promise<void> | Stores and applies the visitor's choice. Works before initialize(). |
getAnalyticsConsent() | AnalyticsConsent | The consent in effect. |
flushAnalytics() | Promise<AnalyticsFlushStatus> | Sends queued identity links and events now. Resolves with { flushed, pending, lastError? }. |
getFeatureFlags(keys?) | Promise<FeatureFlagsResult> | Evaluates flags, cache first. See Feature flags. |
refreshFeatureFlags(keys?) | Promise<FeatureFlagsResult> | Evaluates flags from the network. |
isFeatureEnabled(key) | boolean | The last evaluated value, synchronously. Throws before initialize(). |
getFeatureVariant(key) | FeatureFlagEntry | undefined | The last evaluated flag, synchronously. Throws before initialize(). |
on(event, handler), off(event, handler) | () => void from on | Subscribes to client events. on returns the unsubscribe function. |
onDiagnostic(handler) | () => void | Same as on("diagnostic", handler). |
The client takes and returns plain values; you need no effect code to use it.
Client events
Subscribe with voidhash.on(name, handler).
| Event | Payload | When |
|---|---|---|
initialized | { distinctId } | initialize() has finished. |
identity-changed | { distinctId, previousDistinctId } | After identify(), signOut(), or reset(). |
analytics-consent-changed | { consent } | Consent changed, in this tab or another one. |
analytics-flushed | { accepted, rejected, requestId? } | A batch was delivered. |
analytics-partial-rejection | { accepted, rejected, requestId? } | Voidhash refused some events of a batch. |
feature-flags-updated | { distinctId, keys?, result } | New flag values arrived for the person distinctId. |
diagnostic | VoidhashDiagnostic | See Diagnostics. |
error | { source, message, error? } | A background task failed, such as a scheduled flush. |
React
The @voidhash/web/react entry point has a provider and three hooks. It is a separate entry point,
so pages without React never load it.
import { VoidhashProvider } from "@voidhash/web/react";
<VoidhashProvider config={{ publishableKey: "vh_pk_…" }}>
<App />
</VoidhashProvider>;VoidhashProvider takes either config, the client options, or client, a client you created.
- With
config, the provider creates the client, initializes it when it mounts, and destroys it when it unmounts. React Strict Mode's immediate remount keeps the same client, and a provider shown again after a Suspense boundary or<Activity>hid it starts a new one. - With
client, the provider initializes it but never destroys it, so code outside React can keep using the same client.
The provider reads config and client once, when it mounts; to change them, mount a new provider,
for example with a key.
| Hook | Returns |
|---|---|
useVoidhash() | { client, status, isInitialized, initError, retryInit, distinctId }. distinctId follows identify() and signOut(). |
useAnalytics() | { track, page, flushAnalytics, consent, enabled, setAnalyticsConsent }. Follows consent changes in other tabs. |
useFeatureFlags(keys?) | { data, isEnabled, getVariant, isLoading, isStale, isExpired, error, refetch }. |
Hooks throw when they are used outside a VoidhashProvider. The React entry point is marked
"use client", so the Next.js App Router renders it on the client; the
quickstart shows the setup.
status is initializing until the client is ready, then ready. If initialize() fails, for
example because the page's storage is blocked in a way the SDK cannot work around, it is failed,
initError says why, and retryInit() tries again:
const { status, initError, retryInit } = useVoidhash();
if (status === "failed") {
reportError(initError);
}Analytics never block rendering, so most apps render as usual in every status.
Errors
Rejections are VoidhashError instances, exported from @voidhash/web:
| Class | Meaning |
|---|---|
VoidhashNotInitializedError | The method needs initialize() to resolve first. |
VoidhashDestroyedError | The client was destroyed. Create a new one. |
VoidhashConfigurationError | An option is invalid, such as a secret key or a relative baseUrl. |
VoidhashInvalidArgumentError | An argument is invalid, such as a blank user ID. Nothing was changed. |
VoidhashError | An operation failed. Its message starts with a code such as FAILED_TO_RESET. |
Background work reports through diagnostics instead of rejecting.
Bundle size
The SDK's own code is about 19 KB gzipped. With the parts of effect it uses, a production build of
a page that does not use effect otherwise grows by about 65 KB gzipped; an app that already uses
effect shares it. Nothing is loaded from a CDN at runtime. To keep a landing page's first paint
fast, initialize the client after your critical content, for example in an effect or with a
dynamic import().
Not available on the web
The web SDK covers analytics, attribution, identity, and feature flags. These are not part of it:
- Purchases and restore. Web purchases go through your own Stripe integration and reach Voidhash through Stripe. See Stripe revenue.
- Paywalls. Hosted paywalls are presented by the mobile SDKs.
- Push notifications and deep links.
- Access checks in the browser. Check access on your server with the Node.js library.