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
| Data | When | Used for |
|---|---|---|
| Anonymous ID | Always; one per browser. | Identity, analytics |
| User ID | When you call identify(). | Identity, analytics |
| Email, name and traits | When you pass them to identify() (sent even without analytics consent) or set person attributes. | Identity |
| Pages and product events | While consent is granted: page origin, path and title, referrer origin and path, and the events you track. | Analytics |
| Campaigns and ad clicks | While consent is granted: utm_* parameters, ad IDs, click IDs, and Meta's _fbc and _fbp cookies if your site sets them. | Attribution |
| Device details | With every request: user agent, languages, platform. With events: time zone, screen and viewport size. | Analytics |
| IP address and location | With 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
localStorageorsessionStorage. The anonymous ID, flags andidentify()links live in memory, so each page load is a new anonymous visitor. - Nothing is sent unless you evaluate feature flags, call
identify()orsetPersonAttributesSync(), or callsetAdDataSharing(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
- 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.
- In their browser, call
reset(), so later events no longer carry their ID. - 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-js | Voidhash web SDK |
|---|---|
before_send, sanitize_properties, property_denylist | analytics.beforeSend. The page address is in the event's context. |
Rewriting $current_url, $pathname or $referrer | analytics.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: true | analyticsConsent: "pending" |
| Opting in and out from a consent tool | analyticsConsentSource: "app" |
opt_out_persistence_by_default: true, cookieless_mode: "on_reject" | storeBeforeConsent: false |
cross_subdomain_cookie: true | crossSubdomainIdentity: true |
capture_pageview: "history_change" | The default |
capture_pageview: false | analytics: { capturePageViews: false } |
| None | pauseAnalytics() and resumeAnalytics() |
Next steps
Something wrong or missing on this page? Tell us by email or on Discord.