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

Privacy and consent

What the web SDK collects and stores, how to ask for consent first, and how to erase a visitor.

The web SDK records analytics only while the visitor's analytics consent is granted, and you decide what it may store before then. Use this page for your privacy policy and cookie banner.

What the SDK collects

DataWhenUsed for
Anonymous IDAlways; one per browser.Identity, analytics
User IDWhen you call identify().Identity, analytics
Email, name and traitsWhen you pass them to identify() (sent even without analytics consent) or set person attributes.Identity
Pages and product eventsWhile consent is granted: page origin, path and title, referrer origin and path, and the events you track.Analytics
Campaigns and ad clicksWhile consent is granted: utm_* parameters, ad IDs, click IDs, and Meta's _fbc and _fbp cookies if your site sets them.Attribution
Device detailsWith every request: user agent, languages, platform. With events: time zone, screen and viewport size.Analytics
IP address and locationWith every event while consent is granted. Voidhash derives country, region, city, time zone and approximate coordinates.Analytics

The SDK sends data only to Voidhash, or to the proxy or tracking domain you set. It records no part of the address but the path and campaign parameters, and its requests carry at most your site's origin as Referer. Behind a proxy you run, events carry the proxy's location rather than the visitor's.

IP addresses

Voidhash keeps the full IP address of an event for 90 days, then removes it within a day. The event keeps only the network part, as $ip: the first three parts of an IPv4 address, such as 203.0.113.0, or the first 48 bits of an IPv6 address. Erasing a person removes both.

What the SDK stores in the browser

The SDK keeps its state in localStorage under keys that start with vh:2:, and uses sessionStorage for a campaign seen while consent is pending. It sets a cookie only with crossSubdomainIdentity on; list that one in your cookie banner. Withdrawing consent removes every analytics key and the cookie. Browser storage lists every key and the cookie's attributes.

Ask for consent first

Where the law requires consent before a site stores an identifier or records analytics, as in the EU, start the client with consent pending, keep its state off the device, and pass every answer from your banner:

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

export const voidhash = createVoidhashClient({
  analyticsConsent: "pending",
  publishableKey: "vh_pk_...",
  storeBeforeConsent: false,
});
void voidhash.initialize();

export function onConsentChange(analyticsAllowed: boolean) {
  void voidhash.setAnalyticsConsent(analyticsAllowed ? "granted" : "denied");
}

Until the visitor answers:

  • Nothing is recorded, and nothing is written to localStorage or sessionStorage. The anonymous ID, flags and identify() links live in memory, so each page load is a new anonymous visitor.
  • Nothing is sent unless you evaluate feature flags, call identify() or setPersonAttributesSync(), or call setAdDataSharing(false). Those requests are marked as made without consent, so Voidhash records no analytics for them. identify() still sends the email, name and traits you pass; hold it until consent if your regime treats that as analytics.
  • The campaign the visitor arrived from is held in memory across client-side navigations. A full page load before they answer loses it, so show the banner on landing pages.

When the visitor answers granted, the SDK records from then on, stores the page's state, and records the held campaign with the time of the click. Other open tabs follow. When they answer denied, the choice is the only thing stored. Withdrawing consent later discards every queued event in every tab, removes the SDK's state from the device, and deletes the cross-subdomain cookie.

The stored choice wins over the analyticsConsent option on later visits, so your own banner asks only once. In React, useAnalytics() returns consent and setAnalyticsConsent, and re-renders when consent changes in any tab.

With the default storeBeforeConsent: true, the SDK stores its anonymous ID, cached flags, identify() links and, while pending, the campaign for the browser session. The visitor then stays one visitor across page loads before answering.

When someone rejects everything

Analytics consent and agreement to share purchases with ad platforms are separate. When a visitor rejects everything, call both:

void voidhash.setAnalyticsConsent("denied");
void voidhash.setAdDataSharing(false);

setAdDataSharing(false) works whatever analytics consent says. It is stored and retried until Voidhash writes it, and records no analytics. If Voidhash has no such person yet, the SDK retries with growing waits for up to 48 hours, then drops it with an AD_DATA_SHARING_REVOCATION_DROPPED diagnostic. A browser keeps up to 20 pending revocations, so call it when the visitor answers, not on every page load. Agreeing again with setAdDataSharing(true) needs analytics consent.

Use a consent tool

With a consent tool such as Cookiebot or OneTrust, make it the only source of consent with analyticsConsentSource: "app" and pass it every answer. See Connect a consent tool.

Keep private pages out of analytics

To keep pages whose address carries personal data out of analytics, return null for them from analytics.sanitizeUrl. To strip properties, use analytics.beforeSend. See Keep data out of analytics.

Erase a visitor's data

  1. Request an erasure from your backend or in Studio. It removes the person, their events and stored IP addresses, and the anonymous profiles merged into them.
  2. In their browser, call reset(), so later events no longer carry their ID.
  3. To remove everything the SDK stored in the browser too, deny consent (which deletes the cross-subdomain cookie), destroy the client and delete its keys. This also removes the consent choice, so the visitor is asked again. Repeat on each subdomain.
await voidhash.setAnalyticsConsent("denied");
await voidhash.destroy();
for (const storage of [localStorage, sessionStorage]) {
  for (const key of Object.keys(storage)) {
    if (key.startsWith("vh:") || key.startsWith("@voidhash/web:")) storage.removeItem(key);
  }
}

Coming from posthog-js

posthog-jsVoidhash web SDK
before_send, sanitize_properties, property_denylistanalytics.beforeSend. The page address is in the event's context.
Rewriting $current_url, $pathname or $referreranalytics.sanitizeUrl, which also covers a campaign's landing path
opt_in_capturing(), opt_out_capturing()setAnalyticsConsent("granted"), setAnalyticsConsent("denied")
has_opted_out_capturing()getAnalyticsConsent() !== "granted"
opt_out_capturing_by_default: trueanalyticsConsent: "pending"
Opting in and out from a consent toolanalyticsConsentSource: "app"
opt_out_persistence_by_default: true, cookieless_mode: "on_reject"storeBeforeConsent: false
cross_subdomain_cookie: truecrossSubdomainIdentity: true
capture_pageview: "history_change"The default
capture_pageview: falseanalytics: { capturePageViews: false }
NonepauseAnalytics() and resumeAnalytics()

Next steps

  • Connect a consent tool
  • Keep data out of analytics
  • Delete personal data
  • Send requests through your own domain

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

PreviousPage views and attributionNextBrowser storage

On this page

What the SDK collectsIP addressesWhat the SDK stores in the browserAsk for consent firstWhen someone rejects everythingUse a consent toolKeep private pages out of analyticsErase a visitor's dataComing from posthog-jsNext steps