Web
Vouch in a browser, where attribution is exact because there is no store to lose the click in.
A web app is a first-class Vouch client. It gets a device, a session, an identify and events, and it lands in the same funnel and the same segments as a phone.
Attribution on the web is exact, which is the opposite of the mobile story. A Vouch link redirects straight to your page carrying a single-use token, and the SDK hands it back. There is no store hop to sever the click from the arrival, so there is no fingerprinting, no probabilistic matching and no third-party cookie anywhere in the path. The server refuses to guess for the web even when asked.
Install
npm install @vouch/webimport { Vouch } from "@vouch/web";
await Vouch.init({ key: "pk_live_…", appId: "app_…" });That is the whole integration for attribution.
Allow your origin first
Nothing works from a browser until you say which sites may use the key.
Settings → your app → Web origins, or over the API:
PATCH /v1/apps/{app}/settings
{ "web_origins": ["https://acme.com", "https://www.acme.com"] }Until an origin is listed, every browser request is refused with a 403 saying so. Empty is the default, deliberately: a publishable key that silently started working from any page would be a change nobody asked for.
Why this exists, and why it is weaker than mobile
On mobile the publishable key ships inside a signed binary, and the request behind it can be attested: Vouch can tell a real build on a real device from a script. In a browser neither is true. The key is readable by anybody who opens the network tab, and there is no App Attest or Play Integrity to fall back on.
Where the request came from is the only thing left that is cheap to check and hard to fake from another site. So it is checked, on every request, and the answer is an origin you listed or a 403.
Two things follow that are worth knowing rather than discovering:
Exact matches only. Scheme, host and port. https://acme.com does not
admit https://www.acme.com. No wildcards and no subdomain matching, because
a subdomain anybody can claim is the ordinary way an allowlist is defeated.
This is not CORS. CORS asks a browser not to let a page read a response. This refuses to produce one, and a caller that is not a browser ignores CORS entirely and still meets it.
What the SDK does
// Bind the browser to your own user, so referrals and segments find it.
await Vouch.identify("user_42");
// Record something. Batched and sent together.
Vouch.track("checkout.started", { plan: "pro" });
// How this visitor arrived.
Vouch.match; // { type: "session", link_id: "lnk_…", destination: "/summer" }
Vouch.deviceId; // stable for this browserEvents wait a second for company and go as one request, and whatever is queued leaves when the page is hidden or closed. A send that fails keeps its events for next time, and each one carries an id so a resend is recorded once.
Options
key | The app's publishable key. Required |
appId | The app it belongs to. Required |
apiUrl | Where vouchd is. Defaults to the hosted service |
appVersion | Your own release, reported with every event |
autoStart | false to decide something first, then call Vouch.start() |
cleanURL | false to leave the match parameter in the address bar |
Web push
A browser is a third platform for Module G, beside iOS and Android. The same segments, the same frequency caps, the same quiet hours in the recipient's own timezone, and the same send report naming everybody it did not reach.
Enable it once per app. Push settings has a button that generates the app's VAPID keypair and shows the public half. Vouch generates this rather than asking you for it: a VAPID keypair means nothing until a browser subscribes against it, so there is nowhere for you to fetch one from. It is generated once and never regenerated, because every live subscription is bound to the key it was created with.
Save the service worker. Copy vouch-sw.js from the package to the root
of your site. This is the one file Vouch cannot host for you: browsers require
a service worker to come from your own origin. Its scope is the directory it
sits in, so the root covers your whole site.
Already have a service worker? Do not register a second one at the same
scope. The newer registration replaces the older and your existing worker
stops, which usually surfaces as offline support quietly disappearing. Add
importScripts("/vouch-sw.js") to the top of yours instead.
Ask from a click.
const { permission, subscribed } = await Vouch.enablePush({
vapidPublicKey: "BEl62iU…",
});Never on page load. A prompt nobody asked for is refused most of the time, and a refusal is close to permanent: there is no second prompt and the visitor has to find it in site settings. Whatever you gain by asking early you lose for the life of that browser.
A tapped notification opens through your Vouch link and reports itself, so it lands in the same reports as any other click.
What web push cannot do
iOS reaches almost nobody. Safari delivers only to a web app the visitor has added to their home screen. Plan for Chrome, Android and desktop, and do not build a campaign that assumes iPhone reach.
Every push must be visible. userVisibleOnly is mandatory, so there is no
silent background delivery. A push that shows nothing is counted against the
origin, and enough of them revoke the permission.
A subscription can die without telling you. A push service answers 404 or 410 when one is gone, and Vouch retires it then. Nothing else retires a subscription, because treating a 500 as death would lose a subscriber to somebody else's bad afternoon.
Conversions go through your server
There is no conversion() in the web SDK. Post them with a secret key:
POST /v1/conversions
{ "external_user_id": "user_42", "type": "purchase", "amount_minor": 4900, "currency": "gbp" }A publishable key is public, so a browser that could record revenue would be a browser that could invent it. Your server knows what was actually paid; the page only knows what the page was told.
Segments include web devices
A web device carries platform = web, so a segment can name it like any other
lifecycle field: Platform is web, or is one of ios, web. Everything
else works unchanged, because the segment compiler never knew what a platform
was.
What it stores
One device id, in localStorage with a first-party cookie as a fallback, and a
short queue of unsent events. Nothing else, nothing on anybody else's domain,
and no third-party cookie read anywhere. That is possible only because the
token in the URL makes cross-site tracking unnecessary.
Both stores are written because they fail differently: localStorage survives
a cookie purge, the cookie survives code that clears storage on sign-out.
Losing the id is not fatal, it just looks like a new visitor, so nothing throws
when storage is blocked. Safari in private mode and sandboxed iframes both
work.
The single-use token
init takes the match parameter out of the address bar once it has been read.
The token can be redeemed once, so a visitor who copies the URL and shares it
would otherwise hand somebody a spent link, and a page that keeps it puts it in
every referrer header it sends.
Pass cleanURL: false if your router needs the parameter and remove it
yourself.
What the web cannot do
Device attestation. No App Attest, no Play Integrity, no equivalent. A web app gets the origin allowlist and rate limiting, which is real but weaker, and Vouch will not describe it as attestation. This is the one module a web-only customer cannot have in full.
Re-attribution. The first session of a browser's life redeems the token and later ones are opens, so a returning visitor arriving from a second link is not re-attributed. That is the same rule mobile follows, where an install happens once.