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 | Chrome and Edge 93 or later, Firefox 92 or later, and Safari 15.4 or later, on desktop and mobile. The package targets ES2022. |
effect | Peer ^4.0.3. Install 4.0.3 or a later 4.x release; an app that already uses effect 4 keeps its version. |
react | Optional peer, only for @voidhash/web/react. React 18 or later. |
| Frameworks | Any app built with a bundler: Next.js, React Router, TanStack Router, Vue, Svelte, and others. Sites without one, such as Webflow or WordPress, use the script tag. |
| Rendering | Server rendering is safe: creating the client and rendering the provider never touch the DOM. |
| Modules | ES modules for browsers and bundlers, and a script-tag build that includes everything it needs. There is no CommonJS build, as effect is published as ES modules only. |
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
The SDK sends Voidhash the visitor's anonymous ID, your user ID once you identify them, the pages
they view and the events you track, the campaign that brought them, device details, and the IP
address their events come from. It never sets cookies, and it keeps its own state in
localStorage and sessionStorage, which many consent regimes treat like cookies.
Privacy and consent lists exactly what the SDK collects and what it stores in the browser, for your privacy policy and cookie banner. It also shows how to ask for consent before anything is stored, and how to erase a visitor's data.
Content Security Policy
The SDK only makes fetch requests. If your site sets a Content Security Policy, allow the origin of
baseUrl in connect-src, and the origin of analytics.baseUrl if you set it:
Content-Security-Policy: connect-src 'self' https://api.voidhash.comWith a first-party proxy on your own origin, 'self' covers it; with
a tracking domain, allow that domain instead of api.voidhash.com. The
SDK loads no scripts, styles, images, fonts, or workers and never evaluates code, so it needs
nothing in other directives, and no 'unsafe-eval'.
Event batches are sent as text/plain without custom headers, so a cross-origin batch needs no CORS
preflight request. Feature flag, identify(), person, and checkout requests carry the SDK's own
x- headers, so the browser sends a preflight OPTIONS request before them, which Voidhash answers. Through a
proxy on your own origin, no request needs a preflight.
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. |
storeBeforeConsent | true | false keeps the SDK's state in memory, not in browser storage, until analytics consent is granted. See Ask for consent first. |
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. |
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, at most 100; a larger value counts as 100. Reaching it sends the batch at once. |
maxBatchBytes | 262144 | Upper bound of the events in one request, in bytes. An event larger than this is sent on its own. |
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; with storeBeforeConsent: false, only once consent is granted. |
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 and gives up after 10 seconds. 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 or does not answer within 10 seconds. |
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 } | A batch was delivered. |
analytics-partial-rejection | { accepted, rejected } | 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
With the parts of effect it uses, the SDK adds about 36.4 kB gzipped to a page that does not use
effect otherwise, and the React entry about 1.6 kB more. About 21 kB of that is the SDK's own
code; an app that already uses effect shares the rest. The script-tag file,
which includes everything, is about 36.9 kB gzipped and loads from Voidhash's CDN. The npm package
loads nothing 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.