Docs·Tracking
/1.0/track/session-endEnd a session
Closes a visit and gives it the numbers only the browser knows: how long the visitor stayed, how many pages they saw, and whether the visit was a bounce. A visit's duration and bounce come from here and nowhere else. Without session ends, every visit has a duration of zero and none counts as a bounce, so if you send pageviews yourself, send these as well.
The tracking script sends a session end every time the tab is hidden or the page is closed. When a
visitor comes back after more than 30 minutes without activity, it also sends one for the visit that
ran out before starting a new one. It uses fetch with the keepalive option, which lets a request
finish after the page has gone. One visit therefore usually gets several session ends,
one for each time the visitor switches away from the tab. That is expected: each one replaces the
numbers of the one before.
The request
POST https://sdk.spreelytics.com/1.0/track/session-end. Unlike the other tracking routes, this one
needs clientKey inside the body, in both encodings:
- Form, as the script sends it:
Content-Type: application/x-www-form-urlencoded, with aclientKeyfield and adatafield holding the JSON object below as a string. The JSON indatamust containclientKeyas well. - JSON:
Content-Type: application/json, with the JSON object below,clientKeyincluded, as the body. A key in the query string is accepted too, but does not replace the one in the body.
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 |
|---|---|---|---|
clientKey | string | Yes | Your client key. Required in the body on this route. |
visitorId | string | Yes | The id of the browser, the same one the visit's pageviews carry. |
sessionId | string | Yes | The visit being closed. |
timestamp | string | Yes | When the visit ended, as an ISO 8601 date and time in UTC, such as 2026-10-01T09:41:00.000Z. |
exitPage | string | No | The full address of the last page the visitor saw. It becomes the visit's exit page. It has to be a full address, starting with https:// or http://: anything else makes Spreelytics skip the session end. Without it, the visit keeps the last page it recorded. |
timeOnLastPage | number | No | Milliseconds spent on the last page, counting only the time the tab was in view. It is recorded as the time spent on the exit page. |
totalDuration | number | No | The visit's length in milliseconds. The script sends the time its tabs were in view, from the first page on. Without it, the duration is the time from the visit's first pageview to this session end. |
pageCount | number | No | The pages seen in the visit. It replaces the count Spreelytics kept from the visit's pageviews. |
eventCount | number | No | The events the visitor triggered in the visit, not counting scroll-depth events. |
isBounce | boolean | No | Whether the visit was a bounce. When you send it, it is the answer. |
hadInteractionOnLastPage | boolean | No | Whether the visitor did anything on the last page. The script sends true when the visit had any event. |
contactId | string | No | Accepted and ignored. |
Any other field makes the request fail with 400.
Bounce and duration
A bounce is a visit with one page and no events. Time plays no part: a visitor who reads one long
article for six minutes and does nothing else is a bounce, and one who clicks a tracked button after
three seconds is not. The script works it out this way before it sends isBounce:
const isBounce = pageCount <= 1 && eventCount === 0;
What Spreelytics does with the counters depends on which ones you send:
| You send | Bounce | Duration |
|---|---|---|
isBounce | Your isBounce | Your totalDuration, or else the time from the first pageview to this session end |
None of isBounce, pageCount, eventCount and totalDuration | Worked out the same way, from the pageviews and events Spreelytics recorded for the visit | The time from the first pageview to this session end |
pageCount or totalDuration, without isBounce | Unchanged from what the visit already had | Your totalDuration, or else the time from the first pageview |
If you do not count pages and events yourself, send none of the four counters. Spreelytics then uses its own counts, and the visit comes out right.
How session ends are handled
- Each session end overwrites the visit's end time, exit page, duration and bounce. Only the last one counts.
- A pageview in the same session after a session end reopens the visit. The next session end closes it again with the new numbers.
- A session end for a visit Spreelytics does not hold is skipped: one with no pageview recorded, or one where nothing has arrived for 30 minutes.
- Session ends are not counted towards your plan's allowance, and the hard limit on that allowance does not stop them.
What it answers
| Answer | What it means |
|---|---|
200 with an empty body | Accepted. The visit is updated shortly after. |
200 with the body OK | Dropped and not stored. See below. |
400 | A required field is missing (clientKey in the body included) or has the wrong type, or the body has a field this route does not accept. 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 session end 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, and when it comes from an address in the published
ranges of Amazon Web Services, Google Cloud, Oracle Cloud, DigitalOcean or Linode. The full rules are
in What the Tracking API drops.
When to send it yourself
Send session ends yourself only when you send the visit's pageviews yourself. On a site where the script runs, it already sends them, and yours would overwrite its numbers.
From a server, the same checks apply as for pageviews: a server on Amazon Web Services, Google Cloud,
Oracle Cloud, DigitalOcean or Linode is dropped, the User-Agent has to be a browser's (forward the
visitor's), and an audience with Allowed domains needs an Origin header with a hostname from the
list. See Calling the Tracking API from a server.
On a cookieless audience
With cookieless tracking on, the collector ignores the visitorId and sessionId
you send, and removes isBounce, pageCount, eventCount and totalDuration from the body. It
works out the visitor and the visit from the request's address and User-Agent, the duration from the
first pageview to the session end, and the bounce from the pageviews and events it recorded. From a
server, the visitor it works out is made from your server's address, so the session end would close
the wrong visit.
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/session-end' \
-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 '{
"clientKey": "YOUR_CLIENT_KEY",
"visitorId": "vid_6f1c2a4e-8d3b-4c55-9a7e-2b8f0c9d1e23",
"sessionId": "sess_0b7d9e4a-3c21-4f6a-8e5d-7a9c1b2d3e4f",
"exitPage": "https://www.example.com/pricing",
"timeOnLastPage": 12400,
"totalDuration": 45000,
"pageCount": 3,
"eventCount": 1,
"isBounce": false,
"hadInteractionOnLastPage": true,
"timestamp": "2026-10-01T09:41:00.000Z"
}'
// In a browser, without the tracking script, the way the script does it:
// each time the tab is hidden, with keepalive so the request outlives the
// page. These are the ids saved by the browser sample on Track a pageview.
document.addEventListener('visibilitychange', () => {
if (document.visibilityState !== 'hidden') return;
const ids = JSON.parse(localStorage.getItem('my_ids'));
fetch('https://sdk.spreelytics.com/1.0/track/session-end', {
method: 'POST',
keepalive: true,
body: new URLSearchParams({
clientKey: 'YOUR_CLIENT_KEY',
data: JSON.stringify({
clientKey: 'YOUR_CLIENT_KEY', // this route needs it inside data as well
visitorId: ids.visitorId,
sessionId: ids.sessionId,
exitPage: location.href,
// No counters: Spreelytics works out pages, duration and bounce itself.
timestamp: new Date().toISOString(),
}),
}),
});
});
# On your server, when a visit you tracked from the server is over.
from datetime import datetime, timezone
import requests
requests.post(
"https://sdk.spreelytics.com/1.0/track/session-end",
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={
"clientKey": "YOUR_CLIENT_KEY",
"visitorId": visitor_id,
"sessionId": session_id,
"exitPage": last_page_url,
"timestamp": datetime.now(timezone.utc).isoformat(timespec="milliseconds").replace("+00:00", "Z"),
},
)
Response
200 OK
(empty body)
Related
- Track a pageview: the pageviews a session end closes.
- Autocapture: when the script sends session ends on its own.
- Getting started: keys, encodings and the rules every tracking call shares.