Docs·Tracking

POST/1.0/track/session-end

End 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 a clientKey field and a data field holding the JSON object below as a string. The JSON in data must contain clientKey as well.
  • JSON: Content-Type: application/json, with the JSON object below, clientKey included, 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

FieldTypeRequiredWhat it is
clientKeystringYesYour client key. Required in the body on this route.
visitorIdstringYesThe id of the browser, the same one the visit's pageviews carry.
sessionIdstringYesThe visit being closed.
timestampstringYesWhen the visit ended, as an ISO 8601 date and time in UTC, such as 2026-10-01T09:41:00.000Z.
exitPagestringNoThe 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.
timeOnLastPagenumberNoMilliseconds 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.
totalDurationnumberNoThe 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.
pageCountnumberNoThe pages seen in the visit. It replaces the count Spreelytics kept from the visit's pageviews.
eventCountnumberNoThe events the visitor triggered in the visit, not counting scroll-depth events.
isBouncebooleanNoWhether the visit was a bounce. When you send it, it is the answer.
hadInteractionOnLastPagebooleanNoWhether the visitor did anything on the last page. The script sends true when the visit had any event.
contactIdstringNoAccepted 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 sendBounceDuration
isBounceYour isBounceYour totalDuration, or else the time from the first pageview to this session end
None of isBounce, pageCount, eventCount and totalDurationWorked out the same way, from the pageviews and events Spreelytics recorded for the visitThe time from the first pageview to this session end
pageCount or totalDuration, without isBounceUnchanged from what the visit already hadYour 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

AnswerWhat it means
200 with an empty bodyAccepted. The visit is updated shortly after.
200 with the body OKDropped and not stored. See below.
400A 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: UnauthorisedNo client key, an unknown client key, or a request from a site that is not in Allowed domains (Settings → Details).
413The body is larger than 250,000 bytes.
500In 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)

See your own numbers today.

Free up to 100k events a month · no credit card

Contact

Tell us what you need.

A real person reads these. We usually reply within one working day.

Or email us directly at contact@spreelytics.com.