# Web SDK

> Install @whisperr/web to send identify calls and events from any browser app, with anonymous visitors, app language and an exit flush.

`@whisperr/web` sends identify calls and product events from the browser to Whisperr. Use it in any JavaScript app. For React and Next.js, use the [React](/sdks/react/) or [Next.js](/sdks/next/) package, which wraps this one.

## Install

```bash
npm i @whisperr/web
```

The package is [`@whisperr/web`](https://www.npmjs.com/package/@whisperr/web) on npm. The current version is 0.4.0. It has no runtime dependencies.

## Quickstart

`whisperr.ts`

```ts
import { Whisperr } from "@whisperr/web";


export const whisperr = Whisperr.init({
  apiKey: "wpk_your_publishable_key",
  appVersion: "2.4.0", // optional: your release, sent as context.app_version
});


// After sign-in, and when you restore a signed-in session:
whisperr.identify(user.id, {
  traits: { first_name: user.firstName, plan: user.plan },
  email: user.email,
  emailOptedIn: user.agreedToEmail, // the user's answer in your app; leave it out when the app does not ask
});


// When the user picks a language in your product:
whisperr.setAppLanguage("pt-BR");


// When the user does something in your product. The name is an example:
// use your app's own action.
whisperr.track("report_exported", { format: "pdf" });


// On sign-out:
whisperr.reset();
```

`Whisperr.init()` creates one client per page and returns the same client on later calls. It also sets `window.whisperr`.

Event names must be lowercase `snake_case`. The Web SDK drops any other name before it queues the event and reports it to `onError` as `dropped`.

## Which key goes in the browser?

Use a publishable key (`wpk_…`). In the dashboard it is an **App** key under **Settings → API keys**. It ships in your bundle by design, and anyone can read it. So it cannot change a user’s existing email address or phone number. Whether an email address it sends is opted in depends on your answer to **Does your signup get permission to email users?** See [How do contact channels and consent work in the Web SDK?](#how-do-contact-channels-and-consent-work-in-the-web-sdk)

Never put a server key (`wrk_…`) in browser code. Keep it in your backend secrets and use it with a [server SDK](/sdks/node/).

## What happens before the user signs in?

Before `identify()`, the Web SDK sends events right away under an anonymous id (a UUID v4 kept in `localStorage`). `identify()` sends that anonymous id with the user id, and Whisperr moves the anonymous visitor’s events and state to the user. `reset()` clears the user and starts a new anonymous visitor.

## How does the Web SDK identify returning users?

When a user id is stored from an earlier visit, the Web SDK sends one identify for that user about 2 seconds after page load. It carries the browser timezone and locale and the stored app language, never contact channels. The Web SDK skips it when your app calls `identify()` first, and when the same identify was delivered less than 24 hours ago.

Users who signed in before you installed the SDK have no stored id. Call `identify()` when you restore a session so Whisperr knows them.

## How do I set the message language?

Call `setAppLanguage(tag)` when the user picks a language in your product. Pass a BCP 47 tag such as `de` or `pt-BR`; `_` becomes `-`.

- The Web SDK stores the value and sends it as `traits.app_language`: for the current user, or for the anonymous visitor before sign-in.
- Every later identify carries it. A `traits.app_language` you pass to `identify()` wins.
- Calling it again with the same value sends nothing, so you can call it on every page load.
- `setAppLanguage(null)` clears the stored value. Whisperr keeps the last value it received.
- An invalid tag is ignored (logged when `debug` is on).

Whisperr writes a message in `traits.app_language` first, then in the `context.locale` of the event that triggered it, then in `traits.locale`.

## What the Web SDK adds to identify calls and events

On every `identify()`, the Web SDK fills two traits from the browser unless you pass them yourself:

| Trait      | Source                                                            |
| ---------- | ----------------------------------------------------------------- |
| `timezone` | IANA name from `Intl.DateTimeFormat().resolvedOptions().timeZone` |
| `locale`   | `navigator.language`                                              |

Every event’s `context` carries:

| Key                                   | Value                                                   |
| ------------------------------------- | ------------------------------------------------------- |
| `platform`                            | `web`                                                   |
| `sdk_name`                            | `whisperr-web`                                          |
| `sdk_version`                         | The SDK version, for example `0.4.0`                    |
| `app_version` / `app_build`           | The `appVersion` / `appBuild` options. Not set: no key. |
| `page`                                | `url`, `path`, `referrer`, `title`                      |
| `session_id`, `user_agent`, `library` | Session and browser details                             |

The Web SDK never sets `context.locale`, because Whisperr reads it as the message language for that one event. Pass it in the third argument of `track()` only when you mean that.

## How do contact channels and consent work in the Web SDK?

You can send an email address from the browser with `identify()`, either in `channels` or with the `email` shortcut (with `emailOptedIn` for the user’s choice). The Web SDK runs with the publishable key, so Whisperr ignores `verified: true` from it.

What Whisperr does with the address depends on your answer to **Does your signup get permission to email users?** With **Yes**, the address is opted in unless the app sends `optedIn: false`. With **Per user**, only when the app sends `optedIn: true`. With **No**, or when **Require a server key** is on, Whisperr stores it as pending until your backend confirms it with a [server SDK](/concepts/consent/#confirm-consent-with-identify). `optedIn: false` always records an opt-out. An earlier opt-out, including the unsubscribe link and bounce or complaint suppression, is kept. A later identify from the app never opts the address back in.

App-side consent needs `@whisperr/web`, `@whisperr/react` and `@whisperr/next` 0.4.0 or later. Older versions cannot set it: Whisperr stores the address as pending, whatever your answer. Older versions also send `opted_in: true` on the `email` shortcut by default. See [Contact consent](/concepts/consent/) for the limits on addresses from the app.

## How does the Web SDK deliver events?

- The queue lives in `localStorage`, so events survive a reload. In browsers with the Web Locks API, only one tab sends the shared queue at a time.
- Track events go to `POST /v1/events/batch`; identify calls go to `POST /v1/identify`, in the order you made them.
- When the page is hidden or unloaded, the Web SDK sends what fits in the browser’s keepalive limit with `fetch` and `keepalive`. Events stay queued until Whisperr confirms them, so a lost request is resent on the next load. Whisperr removes duplicates by `$message_id`.
- `429`, `5xx` and network errors are retried with backoff. A `Retry-After` header on `429` or `503` is honored, up to 60 seconds.
- On `401` or `403` the Web SDK stops sending and keeps the queue.
- Pageviews are captured as `page_viewed` on load and on History API navigation. Turn this off with `autocapturePageviews: false`.

See [Delivery, retries, idempotency](/api/delivery/) for the full contract.

## How do I let users opt out of tracking?

`whisperr.optOut()` stops all capture, clears the queue and stores the choice in `localStorage`. `whisperr.optIn()` turns capture back on. Set `respectDoNotTrack: true` to capture nothing when the browser sends Do Not Track.

## Script tag (no build step)

Paste this before `</head>`. The stub queues calls until the script loads.

`index.html`

```html
<script>
!function(){var w=window.whisperr=window.whisperr||[];w._opts={};w.load=function(k,o){w._key=k;w._opts=o||{};var s=document.createElement("script");s.async=1;s.src="https://cdn.jsdelivr.net/npm/@whisperr/web@0.4.0/dist/whisperr.global.js";document.head.appendChild(s)};["identify","track","page","setAppLanguage","flush","reset","optIn","optOut"].forEach(function(m){w[m]=function(){w.push([m].concat([].slice.call(arguments)))}})}();
whisperr.load("wpk_your_publishable_key");
</script>
```

Then call `whisperr.identify(…)` and `whisperr.track(…)` anywhere on the page. The second argument of `whisperr.load()` takes the options below.

## Methods

| Method                                                                                            | What it does                                                                                                                                                       |
| ------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `identify(userId, { traits, channels, email, emailOptedIn, phone, pushToken, preferredChannel })` | Sets the current user and sends their traits and contacts. `emailOptedIn` is the user’s email choice for the `email` shortcut. `channels` overrides the shortcuts. |
| `track(eventType, properties?, context?)`                                                         | Queues one event for the current user or the anonymous visitor.                                                                                                    |
| `page(name?, properties?)`                                                                        | Sends a `page_viewed` event.                                                                                                                                       |
| `setAppLanguage(tag \| null)`                                                                     | Stores the language the user picked in your product.                                                                                                               |
| `flush()`                                                                                         | Sends everything queued. Returns a promise.                                                                                                                        |
| `reset()`                                                                                         | Clears the user and starts a new anonymous visitor.                                                                                                                |
| `optIn()` / `optOut()`                                                                            | Turns capture on or off for this browser.                                                                                                                          |

## Options

| Option                    | Default                    | Notes                                                                      |
| ------------------------- | -------------------------- | -------------------------------------------------------------------------- |
| `apiKey`                  | —                          | Publishable key (`wpk_…`). Required.                                       |
| `baseUrl`                 | `https://api.whisperr.net` | Ingestion base URL.                                                        |
| `appVersion` / `appBuild` | —                          | Sent as `context.app_version` / `context.app_build`.                       |
| `flushAt`                 | `20`                       | Send when this many events are queued.                                     |
| `flushIntervalMs`         | `10000`                    | Send at least this often (ms).                                             |
| `maxQueueSize`            | `1000`                     | Oldest events drop when the queue is full.                                 |
| `maxBatchSize`            | `500`                      | Events per batch request. 500 is the maximum.                              |
| `maxRetries`              | `6`                        | Retries before a send pass stops. Events stay queued.                      |
| `requestTimeoutMs`        | `10000`                    | Per-request timeout (ms).                                                  |
| `autocapturePageviews`    | `true`                     | Captures `page_viewed` on load and navigation.                             |
| `respectDoNotTrack`       | `false`                    | Captures nothing when Do Not Track is on.                                  |
| `persistence`             | `"localStorage"`           | Or `"memory"`.                                                             |
| `disabled`                | `false`                    | Captures and sends nothing.                                                |
| `debug`                   | `false`                    | Logs warnings to the console.                                              |
| `onError`                 | —                          | `(error) => void`. `error.type` is `auth`, `dropped` or `retry_exhausted`. |

## Common questions

### Which API key does @whisperr/web use?

The publishable key (wpk_…), shown as an App key in Settings → API keys. It ships in your bundle by design. Never put the server key (wrk_…) in browser code.

### Are events tracked before login lost?

No. Before identify() the Web SDK sends events under an anonymous id. When you call identify(), it sends the same anonymous id, and Whisperr moves that visitor's events and state to the user.

### Does the Web SDK use cookies?

No. It stores its queue, the anonymous id and the user id in localStorage. When localStorage is unavailable, for example in some private modes, it falls back to memory.

### Can the Web SDK opt a user in to email?

Yes, when you answered Yes or Per user to Does your signup get permission to email users?, Require a server key is off, and the app uses @whisperr/web 0.4.0 or later. With Yes, the address is opted in unless the app sends emailOptedIn: false. With Per user, only when it sends emailOptedIn: true. With No, Whisperr stores it as pending until your backend confirms it with a server SDK.

### Is @whisperr/web safe to import during server-side rendering?

Yes. Outside a browser, Whisperr.init() returns a client that captures and sends nothing.

Source: https://docs.whisperr.net/sdks/web/
