Docs·Tracking
/1.0/track/pageTrack a pageview
Records one pageview. Most numbers in Spreelytics start here: visitors, visits, pages, entry and exit pages, sources and channels all come from pageviews.
The tracking script calls this route on every page load and on every in-app navigation, so on a site where the script runs there is nothing for you to send (see Autocapture). Call it yourself only where the script cannot run: an app or page that cannot load outside JavaScript but can make requests, or a server that records the pages it serves. Before you send from a server, read Calling it from a server. Most server setups need changes, and some cannot work at all.
The request
POST https://sdk.spreelytics.com/1.0/track/page, in either encoding:
- Form, as the script sends it:
Content-Type: application/x-www-form-urlencoded, with aclientKeyfield holding your client key and adatafield holding the JSON object below as a string. From a browser, this goes without a CORS preflight (the extra permission request a browser makes before sending JSON to another site). - JSON:
Content-Type: application/json, the JSON object below as the body, and the key in the address:?clientKey=YOUR_CLIENT_KEY. LeaveclientKeyout of the JSON body. This route does not accept the field and answers400.
Your client key is in Settings → API keys, under Site tracking key. Getting started covers both encodings in more detail.
Fields
| Field | Type | Required | What it is |
|---|---|---|---|
visitorId | string | Yes | The id of the browser. The script makes it from vid_ and a random UUID, keeps it in the spreelytics_vid cookie, and returns it from Spreelytics.getVisitorId(). Keep sending the same id for the same browser: a new id is a new visitor. |
sessionId | string | Yes | The id of the visit. The script makes it from sess_ and a random UUID, starts a new one after 30 minutes without activity, and returns it from Spreelytics.getSessionId(). |
url | string | Yes | The full address of the page, starting with https:// or http://. A path on its own, such as /pricing, is accepted and then thrown away. |
timestamp | string | Yes | When the page was viewed, as an ISO 8601 date and time in UTC, such as 2026-10-01T09:30:00.000Z. It is stored as sent, so a wrong clock moves the pageview. |
title | string | No | The page's title, as in document.title. |
referrer | string | No | The address the visitor came from, as in document.referrer. Leave it out, or send an empty string, when there is none. |
isEntryPage | boolean | No | true on the first pageview of a visit. See How pageviews make visits. |
isNewVisitor | boolean | No | true while this is the visitor's first visit. It decides whether the visit counts as new or returning. |
timeOnPrevPage | number | No | Milliseconds the visitor spent on the previous page of this visit, counting only the time the tab was in view. The script sends 0 on the first page of a visit. |
prevUrl | string | No | The full address of the previous page of this visit. The script sends it on in-app navigations. When it is missing, Spreelytics uses the last page it recorded for the visit. |
contactId | string | No | Accepted and ignored. Who a visitor is comes from Identify a visitor. |
pageTrackingEnabled | boolean | No | Accepted and ignored. |
Any other field makes the request fail with 400.
What Spreelytics reads from the address
The page is stored as its hostname and its path, without the query string, the # part or a
trailing slash. https://www.example.com/pricing/?plan=pro#faq is stored as /pricing on
www.example.com.
The query string still counts on the first page of a visit, because that is where the visit's source comes from:
| In the address | What it sets |
|---|---|
utm_source, utm_medium, utm_campaign, utm_content, utm_term | The visit's UTM values. utm_source also names the visit's source. |
source or ref | The visit's source, when there is no utm_source |
gclid or msclkid | With a Google or Bing source, puts the visit in Paid Search rather than Organic Search |
Without any of these, the source comes from referrer. A referrer on the page's own site, or on a
subdomain of it, gives no source, and the visit counts as Direct. So does a referrer on Stripe,
PayPal, Paddle, Lemon Squeezy, Polar or Dodo Payments, because a visitor coming back from a payment
page did not arrive from there.
How pageviews make visits
A visit is everything sent with one sessionId.
- The pageview sent with
isEntryPage: truestarts the visit. It fixes the visit's entry page, its source, channel and UTM values, and the visitor's location, browser and device for that visit. If no pageview is marked as the entry, the first one to arrive plays that part. isEntryPage: truealso adds one to the visitor's visit count, thevisitCountthat Get visitor stats returns.- Later pageviews add to the visit's page count and move its exit page. They keep the visit's source, even when their own address carries different UTM tags.
- Spreelytics forgets a visit 30 minutes after the last call that mentions it. Start a new
sessionIdafter 30 minutes without activity, as the script does, and sendisEntryPage: truewith its first pageview. - A visit's duration and whether it bounced come from End a session. If you send pageviews without session ends, every visit has a duration of zero and none counts as a bounce.
Each pageview counts towards your plan's monthly event allowance (see Billing).
Where location, device and browser come from
None of these are fields. Spreelytics reads them from the request itself:
| Detail | Comes from |
|---|---|
| Country, region and city | The address the request comes from |
| Device type: desktop, mobile or tablet | The request's User-Agent header |
| Browser and operating system, with their versions | The request's User-Agent header |
From a browser, all of these describe the visitor. From a server, the location is the server's.
What it answers
| Answer | What it means |
|---|---|
200 with an empty body | Accepted. The pageview appears in the app shortly after. |
200 with the body OK | Dropped and not stored. See below. |
400 | A required field is missing or has the wrong type, or the body has a field this route does not accept, clientKey included. The JSON answer names each problem under fields. |
401 with the body Event API: Unauthorised | No client key, an unknown client key, or a request from a site that is not in Allowed domains (Settings → Details). |
413 | The body is larger than 250,000 bytes. |
500 | In the form encoding, data is not valid JSON. |
A pageview is dropped, and still answered with 200 and OK, when:
- its User-Agent belongs to a bot, a crawler, a command line tool or an HTTP library (curl, Python's requests, axios, node-fetch and others), or names a headless browser or test tool;
- it comes from an address in the published ranges of Amazon Web Services, Google Cloud, Oracle Cloud, DigitalOcean or Linode;
- your workspace has recorded three times its monthly event allowance (one and a half times above 1 million events).
The full rules are in What the Tracking API drops.
Calling it from a server
- Check where your server runs. A request from Amazon Web Services, Google Cloud, Oracle Cloud, DigitalOcean or Linode is dropped, whatever it contains. Vercel and other hosts built on those clouds are included.
- Forward the visitor's User-Agent. Copy the
User-Agentheader from the visitor's request to your server onto the call. Your HTTP library's own User-Agent is dropped as a bot, and the visitor's is what gives the visit its browser, operating system and device. - Send an
Originheader with a hostname from Allowed domains, such ashttps://www.example.com, or the call is refused with401. - Expect your server's location. Country, region and city come from the address the call arrives from, so every visitor you report this way is placed where your server is.
- Do not double count. On pages where the script runs, it already sends the pageview. Send from the server only for pages the script never sees.
On a cookieless audience
With cookieless tracking on, the collector ignores the visitorId, sessionId,
isEntryPage and isNewVisitor you send. It works out the visitor from the request's address and
User-Agent, and the visit from its own 30-minute window. From a browser, that works as it does for
the script. From a server, every call shares the server's address, so visitors whose browsers send
the same User-Agent are counted as one. Send pageviews from a server only to audiences that use the
cookie.
Request
# curl's own User-Agent is dropped as a tool, so this sends a browser's.
curl -X POST 'https://sdk.spreelytics.com/1.0/track/page?clientKey=YOUR_CLIENT_KEY' \
-H 'Content-Type: application/json' \
-H 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36' \
-H 'Origin: https://www.example.com' \
-d '{
"visitorId": "vid_6f1c2a4e-8d3b-4c55-9a7e-2b8f0c9d1e23",
"sessionId": "sess_0b7d9e4a-3c21-4f6a-8e5d-7a9c1b2d3e4f",
"url": "https://www.example.com/pricing?utm_source=newsletter",
"title": "Pricing",
"referrer": "https://www.google.com/",
"isEntryPage": true,
"isNewVisitor": true,
"timestamp": "2026-10-01T09:30:00.000Z"
}'
// In a browser, without the tracking script, you keep the ids yourself:
// one visitor id per browser, and a new session id after 30 idle minutes.
const ids = JSON.parse(localStorage.getItem('my_ids') ?? 'null') ?? {
visitorId: `vid_${crypto.randomUUID()}`,
};
const isEntryPage = !ids.sessionId || Date.now() - ids.lastSeen > 30 * 60 * 1000;
if (isEntryPage) ids.sessionId = `sess_${crypto.randomUUID()}`;
ids.firstSessionId ??= ids.sessionId;
ids.lastSeen = Date.now();
localStorage.setItem('my_ids', JSON.stringify(ids));
// Form encoding, like the script: no CORS preflight. The browser adds
// the Origin and User-Agent headers itself.
await fetch('https://sdk.spreelytics.com/1.0/track/page', {
method: 'POST',
body: new URLSearchParams({
clientKey: 'YOUR_CLIENT_KEY',
data: JSON.stringify({
visitorId: ids.visitorId,
sessionId: ids.sessionId,
url: location.href,
title: document.title,
referrer: document.referrer,
isEntryPage,
isNewVisitor: ids.sessionId === ids.firstSessionId,
timestamp: new Date().toISOString(),
}),
}),
});
# On your server, for a page the tracking script never sees. visitor_id,
# session_id, referrer and visitor_user_agent come from the visitor's request.
from datetime import datetime, timezone
import requests
requests.post(
"https://sdk.spreelytics.com/1.0/track/page",
params={"clientKey": "YOUR_CLIENT_KEY"},
headers={
# The visitor's own User-Agent. The requests default is dropped as a tool.
"User-Agent": visitor_user_agent,
# A hostname from Allowed domains.
"Origin": "https://www.example.com",
},
json={
"visitorId": visitor_id,
"sessionId": session_id,
"url": "https://www.example.com/pricing",
"title": "Pricing",
"referrer": referrer,
"isEntryPage": True,
"isNewVisitor": False,
"timestamp": datetime.now(timezone.utc).isoformat(timespec="milliseconds").replace("+00:00", "Z"),
},
)
Response
200 OK
(empty body)
Related
- End a session: the call that gives a visit its duration and bounce.
- Autocapture: what the script records without any code.
- Getting started: keys, encodings and the rules every tracking call shares.