diff --git a/guides/web-setup.md b/guides/web-setup.md
index 0a97303..33ad889 100644
--- a/guides/web-setup.md
+++ b/guides/web-setup.md
@@ -25,7 +25,7 @@ order: 250
Once you have your App ID, edit the source code of your website and add the following code snippet to the `
` section of every page, making sure to replace `` with your actual App ID:
```html
-
@@ -41,21 +41,41 @@ You don't need to write any code to use TelemetryDeck. Once you've installed the
If you like, you can switch your [TelemetryDeck Dashboard](https://dashboard.telemetrydeck.com/) to Website mode. To do that, navigate to the relevant app in the Dashboard, click **App Settings** in the sidebar, and change the **Overview Layout** to **Show Data for a blog or static website**.
-## Test Mode
+## Options and parameters
-By default, all events sent from `localhost` or an IP address in the [private IP address ranges](https://en.wikipedia.org/wiki/Private_network#Private_IPv4_address_spaces) are automatically marked as test events. This is to prevent test signals from polluting your data.
+### App ID `data-app-id` (Required)
-It is also possible to mark all signals as test signals by setting the dataset attribute `data-is-test-mode` to `true`.
+### Test Mode `data-is-test-mode` (Optional)
+
+By default, all events sent from `localhost` or an IP address in the [private IP address ranges](https://en.wikipedia.org/wiki/Private_network#Private_IPv4_address_spaces) are automatically marked as test events. This is to prevent test events from polluting your data.
+
+It is also possible to mark all events as test events by setting the dataset attribute `data-is-test-mode` to `true`.
```html
-
```
+To see test events, you can enable **Test Mode** in the Dashboard.
+
+### Disable Engagement `data-page-engagement` (Optional)
+
+If you don't want to collect `TelemetryDeck.Web.pageLeave` events and their metrics, you can disable them by passing `data-page-engagement="false"`:
-To see test signals, you can enable **Test Mode** in the Dashboard.
+
+```html
+
+```
+
+### Custom Ingest Endpoint `data-api` (Optional)
+
+Use the `data-api` parameter to point the Web SDK to a custom TelemetryDeck v3 ingest endpoint.
## Privacy Policy and Opt-Out
@@ -63,22 +83,27 @@ You don't need to update your privacy policy, [but we recommend you do it anyway
## What data is collected?
-Signals automatically contain the following data, although various data points may be missing depending on the user's browser, privacy settings or network connection, or they might be not applicable.
+The TelemetryDeck Web SDK automatically sends two events per page per user to TelemetryDeck: `pageview` and `TelemetryDeck.Web.pageLeave`.
+
+### `pageview`
-### URL Data
+`pageview` events are fired when a page first loads and automatically contain the following data, although various data points may be missing depending on the user's browser, privacy settings or network connection, or they might be not applicable.
+
+#### URL Data
- `url`: The URL of the page that was loaded
+- `path`: The relative path of the page, i.e. the URL minus host and protocol
- `referrer`: The URL of the page that referred the user to this page
-- `type`: The type of event. This is always `pageView` for page views.
+- `type`: The type of event. This is always `pageview` for page views.
-### Origin and Country Data
+#### Origin and Country Data
- `locale`: The locale of the user, e.g. `en-US` or `de-DE`
- `country.isoCode`: The ISO code of the country the user is in, e.g. `US` or `DE`
- `country.isInEuropeanUnion`: Whether the user is in the European Union
- `continent.code`: The code of the continent the user is in, e.g. `EU` or `NA`
-### Campaign and Referrer Data
+#### Campaign and Referrer Data
- `utm_campaign`: The UTM campaign of the page, if any
- `utm_source`: The UTM source of the page, if any
@@ -88,7 +113,7 @@ Signals automatically contain the following data, although various data points m
- `source`: The source parameter of the page, if any
- `ref`: The ref parameter of the page, if any
-### Browser and System Data
+#### Browser and System Data
- `systemVersion`: The version of the operating system the user is using
- `platform`: The platform the user is using, e.g. a Mac or an iPhone or an Android Phone
@@ -101,6 +126,23 @@ Signals automatically contain the following data, although various data points m
- `isTouchCapable`: Whether the user's device supports touch input
- `isBot`: Whether the user is a bot
+### `TelemetryDeck.Web.pageLeave`
+
+`TelemetryDeck.Web.pageLeave` events are fired the first time the page is hidden or unloaded: tab switch, tab close, navigation away, app switch on mobile. They do not fire again if the visitor returns to the tab. These events contain information about how much the visitor engaged with the page in question:
+
+#### URL Data
+
+- `url`: The URL of the page that was loaded
+- `path`: The relative path of the page, i.e. the URL minus host and protocol
+- `referrer`: The URL of the page that referred the user to this page
+- `type`: The type of event, i.e. `TelemetryDeck.Web.pageLeave`
+
+#### Engagement Data
+
+- `TelemetryDeck.PageEngagement.scrollDepth`: A number between 0 and 100. The deepest point of the page that has been in the viewport, as a percentage of total page height. A page that fits in the viewport reports 100 without any scrolling.
+- `TelemetryDeck.PageEngagement.scrollDepthMilestone`: String, one of "0", "25", "50", "75", "100": A quantified version of scroll depth for better clustering
+- `TelemetryDeck.PageEngagement.engagedSeconds`: Number, whole seconds the page was visible in the foreground (i.e. only while the tab is visible, background time is excluded)
+
## User Identifiers
Our user identifiers are designed to be as privacy-friendly as possible. We do not use cookies or fingerprinting to track users. Instead, combine the IP Address, the App ID, and the User Agent string, and a daily-changing salt to create a unique identifier for each user. This identifier is then hashed using SHA-256 to protect the user's privacy.
@@ -118,6 +160,14 @@ There are different tutorials you should read depending on your use case.
{% endnoteinfo %}
+## What's new in 3.0
+
+Web SDK 3.0, released on September 23, 2026, introduced these changes. If you use the Web SDK without pointing it at a custom API, none of these should be breaking changes:
+
+- We updated the format that web events are sent to a more simplified format
+- We are sending these new events to the new Web v3 API
+- Events no longer contain the `telemetryClientVersion` property. Instead, the property is now named `TelemetryDeck.SDK.nameAndVersion`
+
## What to do next
Now that you've integrated TelemetryDeck, learn how to use the analytics platform to gain valuable insights about your users:
diff --git a/ingest/default-parameters.md b/ingest/default-parameters.md
index a2254e2..80803be 100644
--- a/ingest/default-parameters.md
+++ b/ingest/default-parameters.md
@@ -1,23 +1,23 @@
---
title: Default Parameters
-description: A list of default parameters that our SDKs send with every signal.
-lead: A list of default parameters that our SDKs send with every signal.
+description: A list of default parameters that our SDKs send with every event.
+lead: A list of default parameters that our SDKs send with every event.
---
-Most of TelemetryDeck's SDKs send a set of default parameters with every signal. These are not required to be sent by the user, but are useful. Our default UI will use them to provide more context. All of these parameters are namespaced under `TelemetryDeck` to avoid conflicts.
+Most of TelemetryDeck's SDKs send a set of default parameters with every event. These are not required to be sent by the user, but are useful. Our default UI will use them to provide more context. All of these parameters are namespaced under `TelemetryDeck` to avoid conflicts.
## Main Parameters
-These parameters are sent with every signal and are currently not namespaced. Please avoid using them in your own code.
+These parameters are sent with every event and are currently not namespaced. Please avoid using them in your own code.
- `appID` (String): The ID of the app.
- `clientUser` (String): The ID of the user.
-- `type` (String): The type of the signal.
-- `receivedAt` (Date): The date and time the signal was received by TelemetryDeck.
-- `count` (Int): The number of events sent with the signal (automatically incremented by the server)
-- `isTestMode` (Bool): Whether the signal was sent in test mode.
+- `type` (String): The type of the event.
+- `receivedAt` (Date): The date and time of the event. Ingest API v1 and v2 default this to the time the server received the event and move older dates into the last 24 hours. Ingest API v3 stores the value as sent.
+- `count` (Int): The number of events sent with the event (automatically incremented by the server)
+- `isTestMode` (Bool): Whether the event was sent in test mode. Stored as the string `true` or `false`. Every dashboard query filters on this value.
- `sessionID` (String): The ID of the session.
-- `floatValue` (Double): A float value that can be sent with the signal.
+- `floatValue` (Double): A float value that can be sent with the event.
## App Info
@@ -93,24 +93,31 @@ Information about the user's accessibility device settings to help make apps mor
## Web Analytics
-Web analytics have their own sets of parameters that are usually determined by the web ingest api when you send events from the Web SDK. However, you can send these with any SDK and they will show up in the web analytics dashboard.
+Web analytics have their own sets of parameters that are usually determined by the web ingest API when you send events from the Web SDK. The server derives them from the request on the [v3 web events endpoint](/docs/ingest/v3/#web-events-endpoint) (Web SDK 3.0 and later) and on the older `/v2/w/` endpoint. However, you can send these with any SDK and they will show up in the web analytics dashboard.
-By default the web SDK will send signals of type `pageView` with the following parameters:
+By default the Web SDK sends events of type `pageview` with the following parameters. Web SDK 3.0 and later also send one `TelemetryDeck.Web.pageLeave` event per page load, see [Page Engagement](#page-engagement) below.
These parameters are currently not namespaced. Expect this to change in the future.
+#### Client Data
+
+The Web SDK sends these with every event:
+
+- `locale` (String): The locale of the visitor's browser, e.g. `en-US`.
+- `referrer` (String): The URL of the page that referred the visitor, e.g. `https://example.com`. Empty if there is none.
+- `TelemetryDeck.SDK.name`, `TelemetryDeck.SDK.version`, `TelemetryDeck.SDK.nameAndVersion` (String): See [SDK](#sdk) above. Web SDK 3.0 sends these instead of the deprecated `telemetryClientVersion`.
+
#### URL Data
-- `url` (String): The URL of the page, e.g. `https://example.com/about`.
+- `url` (String): The URL of the page without query string and fragment, e.g. `https://example.com/about`.
- `host` (String): The host portion of the URL, e.g. `example.com`.
- `path` (String): The path portion of the URL, e.g. `/about` or `/blog/my-post`.
- `scheme` (String): The scheme portion of the URL, e.g. `https`.
-- `referer` (String): The referrer of the page, e.g. `https://example.com`.
- `TelemetryDeck.Navigation.identifier` (String): String that uniquely identifies the navigation in the format `referrer -> url`.
#### URL Parameters
-TelemetryDeck will extract URL these URL parameters from the url and offer them as parameters attached to the signal:
+TelemetryDeck will extract these URL parameters from the URL and offer them as parameters attached to the event. All other query parameters are dropped.
- `combinedSource` (String): The value of either `ref`, `source`, `utm_source` or `src` in that order of preference.
- `ref`
@@ -137,7 +144,7 @@ TelemetryDeck will extract URL these URL parameters from the url and offer them
- `majorMinorSystemVersion` (String): The major and minor version of the operating system, extracted from the user agent string.
- `platform` (String): The platform of the device (macOS, iOS, Windows, etc.), extracted from the user agent string.
- `modelName` (String): The model name of the device, extracted from the user agent string.
-- `browserName` (String): The browser family, extracted from the user agent string.
+- `browser` (String): The browser family, extracted from the user agent string.
- `browserVersion` (String): The browser version, extracted from the user agent string.
- `device` (String): The type of device, extracted from the user agent string.
- `isMobile` (Bool): Whether the device is a mobile device, extracted from the user agent string.
@@ -146,9 +153,19 @@ TelemetryDeck will extract URL these URL parameters from the url and offer them
- `isDesktop` (Bool): Whether the device is a desktop, extracted from the user agent string.
- `isBot` (Bool): Whether the device is a bot, extracted from the user agent string and additional heuristics.
+Events from the v3 web endpoint store these booleans as `true` and `false`. Events from the older `/v2/w/` endpoint store them as `True` and `False`. Filter on both spellings when you query data from both.
+
+#### Page Engagement
+
+Web SDK 3.0 and later send one event of type `TelemetryDeck.Web.pageLeave` per page load, the first time the page is hidden or unloaded. It carries the URL, country, and browser parameters listed above plus these engagement parameters:
+
+- `TelemetryDeck.PageEngagement.scrollDepth` (Double): The deepest point of the page that has been in the viewport, as a percentage of the page height from `0` to `100`. A page that fits in the viewport reports `100`.
+- `TelemetryDeck.PageEngagement.scrollDepthMilestone` (String): The scroll depth rounded down to one of `0`, `25`, `50`, `75`, or `100`. Use this for grouping.
+- `TelemetryDeck.PageEngagement.engagedSeconds` (Double): Whole seconds the page was visible in the foreground. Time in a background tab is not counted.
+
## Navigation Analytics
-Navigation analytics signals have these parameters, which can be included in any signal type.
+Navigation analytics events have these parameters, which can be included in any event type.
- `TelemetryDeck.Navigation.schemaVersion` (String): The schema version of the navigation. Must be `1`.
- `TelemetryDeck.Navigation.sourcePath` (String): The source path of the navigation, e.g. `/host/info/about` or `app.settings.privacy`.
@@ -174,10 +191,10 @@ Navigation analytics signals have these parameters, which can be included in any
## API
-Information about which TelemetryDeck API this signal was sent to.
+Information about which TelemetryDeck API this event was sent to.
-- `TelemetryDeck.API.Ingest.version` (String): The Ingest API Version this signal was sent to. This will get overwritten by the ingest API server.
-- `TelemetryDeck.API.namespace` (String): The namespace in which to store your data. This usually gets set by the ingest API server when you call it via the `/v2/namespace//` endpoint.
+- `TelemetryDeck.API.Ingest.version` (String): The Ingest API version this event was sent to. The ingest API server always sets this value. It is `v1` for the v1 API, `v2` for the v2 API, `v2web` for the v2 web endpoint, `events.v3` for the [v3 events endpoint](/docs/ingest/v3/#events-endpoint), and `web.v3` for the [v3 web events endpoint](/docs/ingest/v3/#web-events-endpoint).
+- `TelemetryDeck.API.namespace` (String): The namespace in which to store your data. The ingest API server sets this from the URL when you call the `/v2/namespace//` or `/v3//` endpoint, and from the `namespace` property of an event sent to `/v3/w/`.
## RevenueCat Parameters
@@ -211,8 +228,8 @@ These parameters are deprecated because they are not yet namespaced, and will be
- `region` (String): The region of the user.
- `appLanguage` (String): The language of the app.
- `preferredLanguage` (String): The preferred language of the user.
-- `telemetryClientVersion` (String): The version of the TelemetryDeck SDK.
-- `telemetryAPIVersion` (String): The API Version this signal was sent to.
+- `telemetryClientVersion` (String): The version of the TelemetryDeck SDK. Replaced by `TelemetryDeck.SDK.nameAndVersion`. The v2 ingest API copies the value over when the new key is missing.
+- `telemetryAPIVersion` (String): The API version this event was sent to. Replaced by `TelemetryDeck.API.Ingest.version`.
## Reserved Parameters
diff --git a/ingest/v2.md b/ingest/v2.md
index d13bcac..6b2d8e0 100644
--- a/ingest/v2.md
+++ b/ingest/v2.md
@@ -3,7 +3,7 @@ title: Ingest API v2
tags: overview
description: Use the TelemetryDeck Ingest API to send events to TelemetryDeck
lead: Use the TelemetryDeck Ingest API to send events to TelemetryDeck
-order: 0
+order: 1
headerImage: /img/ingestv2.jpg
---
@@ -13,7 +13,7 @@ Usually you'll send events to TelemetryDeck using one of our SDKs. However, if y
Please send your events as a POST request to our ingestion server at `https://nom.telemetrydeck.com/v2/namespace/{namespace}/`, with the headers `Content-Type: application/json` and `charset=utf-8`.
-Replace `{namespace}` with your organization's TelemetryDeck namespace. You can find your namespace in the [TelemetryDeck Dashboard](https://dashboard.telemetrydeck.com/).
+Replace `{namespace}` with your organization's TelemetryDeck [namespace](/docs/articles/namespaces/). You can find your namespace in the [TelemetryDeck Dashboard](https://dashboard.telemetrydeck.com/). If your organization has no namespace yet, send your events to `https://nom.telemetrydeck.com/v2/` instead.
Here's an example in cURL:
@@ -31,22 +31,23 @@ curl -X "POST" "https://nom.telemetrydeck.com/v2/namespace/your-namespace/" \
## Body Structure
-The post body should be an **array of JSON objects**, because you can send multiple events at once. Each signal object **must** have these properties:
+The post body should be an **array of JSON objects**, because you can send multiple events at once. Each event object **must** have these properties:
-| Property | Type | Description |
-| ------------ | ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `appID` | string | Your app's ID |
-| `clientUser` | string | A hash of the user's ID. This should always be the same for the same user. |
-| `type` | string | The type of signal. While it is not enforced, we recommend structuring your signal names in scopes separated by dots, with the signal type beginning with a lowercase letter and any scope beginning with an uppercase letter. |
+| Property | Type | Description |
+| ------------ | ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `appID` | string | Your app's ID. Must not be empty. The server only accepts an empty app ID on the namespace endpoint, because the namespace then identifies where the event belongs. |
+| `clientUser` | string | A hash of the user's ID. This should always be the same for the same user. The server salts and hashes this value again with SHA256 before it stores the event. |
+| `type` | string | The type of event. While it is not enforced, we recommend structuring your event names in scopes separated by dots, with the event type beginning with a lowercase letter and any scope beginning with an uppercase letter. |
The following properties are optional:
-| Property | Type | Description |
-| ------------ | ------ | ------------------------------------------------------------------------------------------- |
-| `sessionID` | string | The user's session ID. This should be the same value for the same session/user combination. |
-| `isTestMode` | Bool | If `true`, the signal will be excluded from production queries. Defaults to `false`. |
-| `floatValue` | number | A numeric measurement. Use for any numeric operations, such as building averages or sums. |
-| `payload` | object | A JSON object with additional data. |
+| Property | Type | Description |
+| ------------ | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
+| `sessionID` | string | The user's session ID. This should be the same value for the same session/user combination. If you leave it out, the server generates a session ID from `appID` and `clientUser` that changes once a day. |
+| `isTestMode` | Bool | If `true`, the event will be excluded from production queries. Defaults to `false`. |
+| `floatValue` | number | A numeric measurement. Use for any numeric operations, such as building averages or sums. |
+| `receivedAt` | string | The date and time at which the event happened, as an ISO 8601 string with time zone, for example `2026-09-24T10:15:00+00:00`. Defaults to the time the server received the event. A date more than 24 hours in the past is moved to exactly 24 hours in the past, and a date in the future is moved to the current time. Use this when you cache events on the device and send them later. |
+| `payload` | object | A JSON object with additional data. |
## Payload
@@ -75,17 +76,33 @@ The payload should not contain nested objects. The behavior of arrays of strings
## Numeric Values
-The `floatValue` field is a top-level property on the signal object (see the optional properties table above). It is not part of the `payload` object. Use it for any numeric operations such as building averages or adding up values.
+The `floatValue` field is a top-level property on the event object (see the optional properties table above). It is not part of the `payload` object. Use it for any numeric operations such as building averages or adding up values.
-{% noteinfo "More special values" %}
-More special top-level fields are coming soon for internal use.
+{% noteinfo "Need other value types?" %}
-- The ones being defined in [this PR](https://github.com/TelemetryDeck/docs/pull/85) need to be implemented server-side first.
-- We're preparing a number of special values specifically for metrics and crashes, but these need to be defined first.
- {% endnoteinfo %}
+The v2 API converts booleans and most other payload values to strings before storing them. Only numbers inside the payload keep their type. If you need to store booleans and numbers as sent, use the [Ingest API v3](/docs/ingest/v3/), which keeps the JSON type of every value.
+
+{% endnoteinfo %}
+
+## Responses
+
+| Status | Body | Meaning |
+| ------ | -------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
+| 200 | `OK` | The events were accepted. |
+| 422 | A JSON error message | The body is empty, not a JSON array, or an event is missing `appID`, `clientUser`, or `type`, or has an empty `appID` and no namespace. |
## Reserved Payload Keys
Some payload keys are reserved for internal use by TelemetryDeck. See the [complete list of reserved parameters](/docs/ingest/default-parameters/#reserved-parameters) which you're not allowed to include in the payload.
In general, avoid using parameter names that begin with the `TelemetryDeck.` scope, as this is reserved for parameters set by TelemetryDeck SDKs and the ingestion server.
+
+## Web Events
+
+Web SDK versions before 3.0 send page views to `https://nom.telemetrydeck.com/v2/w/`. This endpoint takes one event object per request and derives browser, system, location, and campaign data on the server. It keeps working for sites that still run an older Web SDK.
+
+Web SDK 3.0 and later use the [v3 web events endpoint](/docs/ingest/v3/#web-events-endpoint). Use v3 for new website integrations.
+
+## Changelog
+
+- **September 2026**: Events with an empty `appID` are rejected with HTTP 422 unless they carry a namespace. Before, the server accepted them and then dropped them, because they could not be attributed to an app.
diff --git a/ingest/v3.md b/ingest/v3.md
new file mode 100644
index 0000000..169f70a
--- /dev/null
+++ b/ingest/v3.md
@@ -0,0 +1,241 @@
+---
+title: Ingest API v3
+tags: overview
+description: Use the TelemetryDeck Ingest API v3 to send flat events to TelemetryDeck
+lead: Use the TelemetryDeck Ingest API v3 to send flat events to TelemetryDeck
+order: 2
+---
+
+Usually you'll send events to TelemetryDeck using one of our SDKs. However, if you're working with a language or framework that we don't have an SDK for, you can send events directly to our Ingest API.
+
+Ingest API v3 is the newest version of the API. It differs from [v2](/docs/ingest/v2/) in two ways:
+
+- Events are **flat**. There is no `payload` object. Every parameter sits at the top level of the event.
+- Values **keep their JSON type**. A number stays a number, and a boolean stays a boolean.
+
+The v2 API stays available.
+
+## Endpoints
+
+The v3 API has two endpoints. Both live on `https://nom.telemetrydeck.com`.
+
+| Endpoint | Purpose | Request body |
+| ----------------------- | -------------------------------------- | --------------------- |
+| `POST /v3/{namespace}/` | Events from apps, games, and servers | An array of events |
+| `POST /v3/w/` | Website events, as sent by the Web SDK | A single event object |
+
+Send the headers `Content-Type: application/json` and `charset=utf-8`. The trailing slash is optional. The path segment `w` is reserved for the web endpoint, so you cannot use `w` as a namespace.
+
+## Events Endpoint
+
+Send your events as a POST request to `https://nom.telemetrydeck.com/v3/{namespace}/`.
+
+Replace `{namespace}` with your organization's TelemetryDeck [namespace](/docs/articles/namespaces/). You can find your namespace in the [TelemetryDeck Dashboard](https://dashboard.telemetrydeck.com/). The namespace is required in v3. There is no endpoint without one.
+
+Here's an example in cURL:
+
+```bash
+curl -X "POST" "https://nom.telemetrydeck.com/v3/your-namespace/" \
+ -H 'Content-Type: application/json; charset=utf-8' \
+ -d $'[
+ {
+ "appID": "AAAA-BBBBBBBB-CCCC-DDDD",
+ "clientUser": "myClientUserHash",
+ "receivedAt": "2026-09-24T10:15:00+00:00",
+ "type": "Actions.LoginView.loginButtonSucceeded",
+ "sessionID": "mySessionID",
+ "isTestMode": "false",
+ "floatValue": 3.14159,
+ "TelemetryDeck.RunContext.locale": "en_US",
+ "TelemetryDeck.Device.architecture": "x86_64"
+ }
+ ]'
+```
+
+### Body Structure
+
+The post body must be an **array of JSON objects**, because you can send multiple events at once. Each event is a flat object. Required properties, optional properties, and your own parameters all sit at the top level.
+
+Each event **must** have these properties. All three must be strings.
+
+| Property | Type | Description |
+| ------------ | ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `appID` | string | Your app's ID. We recommend always setting it. The server accepts an empty string only because the namespace in the URL already identifies your organization. |
+| `clientUser` | string | An identifier for the user, for example a hash of their user ID. This should always be the same for the same user. The server salts and hashes this value with SHA256 before it stores the event. |
+| `receivedAt` | string | The date and time at which the event happened, as an ISO 8601 string with time zone, for example `2026-09-24T10:15:00+00:00`. Unlike v2, the server stores this value as sent and does not move it into the last 24 hours. |
+
+These properties are optional:
+
+| Property | Type | Description |
+| ------------- | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `type` | string | The type of event. The server accepts an event without a type, but the dashboard needs one, so always send it. We recommend structuring your event names in scopes separated by dots, with the event type beginning with a lowercase letter and any scope beginning with an uppercase letter. |
+| `sessionID` | string | The user's session ID. This should be the same value for the same session/user combination. Unlike v2, the server does not generate a session ID when this is missing. |
+| `isTestMode` | string | Send `"true"` to exclude the event from production queries, otherwise `"false"`. Always send this value: the server adds no default in v3, and every dashboard query filters on it, so an event without it does not show up in the dashboard. |
+| `floatValue` | number | A numeric measurement. Use for any numeric operations, such as building averages or sums. |
+| any other key | any JSON value | Your own parameters. See below. |
+
+### Custom Parameters
+
+Add your own parameters as further top-level keys of the event. Keys should be in the format `Scope.OptionalSubScope.key`, with the scopes capitalized and the key starting with a lowercase letter. This is not enforced but helps you organize your dimensions and fit in better with TelemetryDeck's internal keys.
+
+Values keep their JSON type. Strings, numbers, and booleans are stored as sent. The event must not contain nested objects. The behavior of arrays is left undefined and should be avoided (except if you have a deep knowledge of [multi-value dimensions in Apache Druid](https://druid.apache.org/docs/latest/querying/multi-value-dimensions/)).
+
+The server does not rename or clean your keys in v3. Use only letters, digits, dots, and underscores in key names.
+
+### Properties Added by the Server
+
+The server changes or adds these properties before it stores the event. Everything else passes through untouched.
+
+| Property | Value |
+| ---------------------------------- | --------------------------------------------------------- |
+| `clientUser` | Replaced by the salted SHA256 hash of the value you sent. |
+| `TelemetryDeck.API.namespace` | The namespace from the URL. |
+| `TelemetryDeck.API.Ingest.version` | `events.v3` |
+
+Unlike v2, the server does not add the legacy `payload` list or the `telemetryAPIVersion` property.
+
+### Responses
+
+| Status | Body | Meaning |
+| ------ | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| 200 | The number of accepted events, for example `2` | All events were accepted. An empty array returns `0`. |
+| 422 | A JSON error message | The body is not a JSON array, or at least one event is invalid. The server checks the whole batch before it stores anything, so a single invalid event rejects the whole request and nothing is stored. |
+| 503 | `queue is full, retry later` | The server cannot accept events right now. The response carries a `Retry-After: 5` header. Wait 5 seconds and send the batch again. |
+
+An event is invalid if:
+
+- `appID`, `clientUser`, or `receivedAt` is missing or not a string,
+- `appID` is empty and the namespace in the URL is blank,
+- `receivedAt` is not a valid ISO 8601 date/time.
+
+## Web Events Endpoint
+
+Send website events as a POST request to `https://nom.telemetrydeck.com/v3/w/`. This is the endpoint the [Web SDK](/docs/guides/web-setup/) 3.0 uses. The server derives browser, system, location, and campaign data from the request, so the client only needs to send the URL.
+
+The endpoint takes **exactly one event per request**. The body is a single JSON object, not an array.
+
+The server parses the body as JSON no matter which `Content-Type` header the request carries. The Web SDK uses this to send its last event with `navigator.sendBeacon()` as `text/plain`, which needs no CORS preflight and therefore survives the page unload. The endpoint answers CORS preflight requests for any origin.
+
+Here's an example in cURL:
+
+```bash
+curl -X "POST" "https://nom.telemetrydeck.com/v3/w/" \
+ -H 'Content-Type: application/json; charset=utf-8' \
+ -H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' \
+ -d $'{
+ "appID": "AAAA-BBBBBBBB-CCCC-DDDD",
+ "url": "https://example.com/blog/post?utm_source=newsletter",
+ "referrer": "https://news.example.org/",
+ "locale": "en-US",
+ "isTestMode": "false"
+ }'
+```
+
+### Body Structure
+
+Each web event **must** have these properties:
+
+| Property | Type | Description |
+| ---------------------- | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `url` | string | The full URL of the page, including the query string. Must not be empty. The server splits the URL into its parts and stores it without the query string and fragment. |
+| `appID` or `namespace` | string | Your app's ID, or your organization's [namespace](/docs/articles/namespaces/). At least one of the two must be a non-empty string. You can send both. The server uppercases `appID`. It stores `namespace` as `TelemetryDeck.API.namespace` and removes the `namespace` key. |
+
+These properties are optional:
+
+| Property | Type | Description |
+| ------------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `type` | string | The type of event. Defaults to `pageview`. |
+| `receivedAt` | string | The date and time at which the event happened, as an ISO 8601 string. Defaults to the time the server received the event. |
+| `isTestMode` | boolean or string | If `true` or `"true"`, the event is excluded from production queries. The server stores the value as the string `"true"` or `"false"`. Defaults to `"false"`. |
+| `referrer` | string | The URL of the page that referred the visitor. Used to build `TelemetryDeck.Navigation.identifier`. |
+| `locale` | string | The visitor's locale, for example `en-US`. |
+| any other key | any JSON value | Your own parameters, stored as sent. See [Custom Parameters](#custom-parameters) above for the naming rules. |
+
+Do not send `clientUser` or `sessionID`. The server replaces both with its own visitor identifier, see below.
+
+### Properties Derived by the Server
+
+The server adds these properties to every web event. If your event already contains one of these keys, the server value wins.
+
+From the `User-Agent` header:
+
+- `platform`, `systemVersion`, `majorSystemVersion`, `majorMinorSystemVersion`
+- `browser`, `browserVersion`, `device`, `modelName`
+- `isMobile`, `isTablet`, `isTouchCapable`, `isDesktop`, `isBot` (booleans)
+
+From the `url`:
+
+- `url` without query string and fragment, `host`, `path`, `scheme`
+- `combinedSource`, plus each of `ref`, `source`, `src`, `utm_source`, `utm_medium`, `utm_campaign`, `utm_term`, `utm_content`, `MSCLKID`, and `GCLID` that appears in the query string. All other query parameters are dropped.
+
+From the visitor's IP address:
+
+- `country.isoCode`, `country.isInEuropeanUnion`, `continent.code`
+
+The server reads the IP address from the leftmost entry of the `X-Forwarded-For` header, or from the connection when that header is missing. The IP address itself is never stored.
+
+Other properties:
+
+- `TelemetryDeck.API.Ingest.version` is set to `web.v3`.
+- `TelemetryDeck.Navigation.identifier` is set to `referrer -> url`, with the cleaned `url`.
+
+See [Default Parameters](/docs/ingest/default-parameters/#web-analytics) for a description of each property.
+
+### Visitor Identifiers
+
+Web events use no cookies and no fingerprinting. The server builds `clientUser` from the app ID (or the namespace when there is no app ID), the visitor's IP address, the `User-Agent` string, and a salt that changes every day. It hashes this with SHA256, then hashes the result again with a secret salt. The same visitor of the same site gets the same identifier within a day, and a new one the next day.
+
+`sessionID` is set to the same value as `clientUser`.
+
+This is the same construction that the v2 web endpoint uses, so user counts stay continuous when a site moves from an older Web SDK to version 3.0.
+
+### Responses
+
+| Status | Body | Meaning |
+| ------ | ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
+| 200 | `OK` | The event was accepted. |
+| 422 | A JSON error message | The body is not a JSON object, `url` is missing or empty, neither `appID` nor `namespace` is set, or `receivedAt` is not a valid ISO 8601 date/time. |
+| 503 | `queue is full, retry later` | The server cannot accept events right now. The response carries a `Retry-After: 5` header. |
+
+## Events Sent by the Web SDK
+
+Web SDK 3.0 sends two event types per page load to the web events endpoint. You can send the same events from your own code.
+
+Every event from the Web SDK contains `appID`, `url`, `referrer`, `locale`, `isTestMode` (as the string `"true"` or `"false"`), `TelemetryDeck.SDK.name` (`WebSDK`), `TelemetryDeck.SDK.version`, and `TelemetryDeck.SDK.nameAndVersion`.
+
+### `pageview`
+
+Sent when the page loads. The Web SDK sends no `type`, so the server sets it to `pageview`.
+
+### `TelemetryDeck.Web.pageLeave`
+
+Sent once per page load, the first time the page is hidden or unloaded: tab switch, tab close, navigation away, or app switch on mobile. It does not fire again if the visitor returns to the tab. It carries these engagement parameters in addition to the base parameters:
+
+| Property | Type | Description |
+| --------------------------------------------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
+| `TelemetryDeck.PageEngagement.scrollDepth` | number | The deepest point of the page that has been in the viewport, as a percentage of the page height from `0` to `100`. A page that fits in the viewport reports `100`. |
+| `TelemetryDeck.PageEngagement.scrollDepthMilestone` | string | The scroll depth rounded down to one of `"0"`, `"25"`, `"50"`, `"75"`, or `"100"`. Use this for grouping. |
+| `TelemetryDeck.PageEngagement.engagedSeconds` | number | Whole seconds the page was visible in the foreground. Time in a background tab is not counted. |
+
+Site owners can turn these events off with `data-page-engagement="false"`, see the [Web SDK guide](/docs/guides/web-setup/).
+
+## Differences from v2
+
+| Topic | v2 | v3 |
+| ----------------------------------------------- | ------------------------------------------------------------------ | ---------------------------------------------------------------------- |
+| Custom parameters | Inside the `payload` object | At the top level of the event |
+| Value types | Converted to strings, except numbers inside `payload` | Stored as sent |
+| Namespace | Optional: URL path or `TelemetryDeck.API.namespace` in the payload | Events: required in the URL path. Web: optional `namespace` property |
+| `receivedAt` | Optional, moved into the last 24 hours | Events: required, stored as sent. Web: optional, defaults to now |
+| `sessionID` | Generated once a day when missing | Events: stored as sent, never generated. Web: always set by the server |
+| `isTestMode` | Defaults to `"false"` | Events: no default, always send it. Web: defaults to `"false"` |
+| Web request body | One object with a `payload` | One flat object |
+| Legacy `payload` list and `telemetryAPIVersion` | Added by the server | Not added |
+| `TelemetryDeck.API.Ingest.version` | `v2` for events, `v2web` for web events | `events.v3` for events, `web.v3` for web events |
+| Response body on success | `OK` | Events: the number of accepted events. Web: `OK` |
+
+## Reserved Keys
+
+Some keys are reserved for internal use by TelemetryDeck. See the [complete list of reserved parameters](/docs/ingest/default-parameters/#reserved-parameters) which you're not allowed to include in your events.
+
+In general, avoid using parameter names that begin with the `TelemetryDeck.` scope, as this is reserved for parameters set by TelemetryDeck SDKs and the ingestion server.