Base URL and authentication
Authorization: Bearer <YOUR_API_KEY> (details) and the stats:read permission.
Every endpoint on this page rejects a parameter it does not recognise with a
422, rather than
ignoring it. A filter we cannot apply would otherwise come back as a 200 carrying a number that
answers a different question, and you would have no way to tell. In particular the per-session
endpoints take site_id and nothing else.site_id is the site’s snippet key, the ttid_ value your install snippet carries as data-site. You can read it from GET /v1/sites.
What a session is, and what it is not
Asession_id is an opaque first-party cookie value. It groups one visit. It is not a person, it is not an account, and it does not identify anyone by itself.
Traceten never returns an IP address, an ip_hash, a user agent, a language or a timezone from these endpoints, and visitor_id is never returned at any depth in any response on this page.
Page URLs
A page URL arrives by two paths with two different guarantees, so no single statement covers it. From the browser snippet,sanitizeUrl (packages/snippet/src/track.ts:389-415) rewrites the URL before it is sent. It works only on the QUERY STRING, and there it does exactly three things:
- It replaces the VALUE of any query parameter whose name matches one of 19 exact names, compared case-insensitively:
email,e,mail,phone,tel,name,firstname,lastname,fname,lname,token,access_token,id_token,code,state,password,pwd,ssn,dob(thePII_PARAMSlist,packages/snippet/src/track.ts:182-202). - It also replaces the value of any parameter whose name contains
token,secretorkey. - Replaced values become the literal string
REDACTED. The parameter NAME is kept.
-
The path is never touched.
/u/jane.doe@example.comand/reset/<token>arrive intact. -
The fragment (
#...) is never examined. The redaction readsURL.searchParams, which cannot see past the#, and the fragment survives into the value we store. An OAuth implicit-flow callback such ashttps://example.com/cb#access_token=eyJ...&id_token=...is sent verbatim, even thoughaccess_tokenandid_tokenare on the list above. Being in the fragment is what makes the difference:If your app receives tokens in a fragment, do not let Traceten see that page. Strip the hash before the snippet runs, or exclude the callback route. -
Any query parameter not on that list is sent as-is, including ones that plainly carry personal data under another name, such as
?customer=jane.doe@example.comor?address=.... - If the URL cannot be parsed, it is sent unchanged.
sanitizeUrl never executes and the URL is stored exactly as your code supplied it. If you send events from your server, scrub URLs before you emit them. Nothing downstream does it for you.
A session is one visit. If your question counts people rather than visits, use the Visitors API, which returns one row per visitor and can be filtered by source, campaign, landing page, country, city, device and browser. A visitor row carries latest_session_id, so a person leads to their visits, and each visit opens here with /detail.
For what a visit actually did, event by event, use GET /v1/visitors/{handle}/timeline. It returns the events for a page of one person’s visits, so a single request covers the trail across every visit they made rather than one visit at a time.
Neither direction accepts an identity. No endpoint takes a name, an email address, an IP address or a visitor_id as an input, and none returns one. Visitor rows are addressed by an opaque handle that is sealed against a single site, so a handle from one site returns 404 on another, and nothing links a person’s activity across two customers.
GET /v1/sessions
One row per session, newest first, with the AI source, the confidence and the detection method behind it.
The window, and what happens when you ask for more than we kept
If you send neitherfrom nor to, the window is the last 30 days, not all of history. Send both together, or neither: a partial range is a 422.
You may ask for a window reaching further back than we retain, and you will get an answer. It will cover only the part we still hold. Session records are guaranteed for at least 730 days, and past that boundary the store does not report “no data”, it reports a smaller number, precisely and confidently.
So meta always tells you which window was actually answered:
meta is present on every response, clamped or not, so you never have to branch on its absence. Label your charts with effective_window, not with what you asked for.
Query parameters
Paging depth
(page - 1) * page_size may not exceed 100,000. Past that you get a 422.
The database reads every row it skips, so a very deep page is the most expensive request this endpoint can serve. If you are paging that far you want a narrower window or a filter, not a higher page number.
campaign is a substring search rather than an exact match because campaign names are free text with no canonical form: spring_sale and spring-sale are two different campaigns, and an exact filter would silently return nothing for a near miss.
Reading the response
is_dark_traffic is true when no header identified the source, so the classification came from other signals rather than a Referer or a utm_source. This is the ~70% of AI traffic that arrives with nothing to read, and it is the reason this product exists.
stage is a number identifying which detection check made the decision, recorded so you can debug a classification alongside detection_method. 0 means no AI source was identified.
active_time_seconds is foreground time on page, summed from what the snippet measured. It is not wall-clock session duration. 0 means nothing was measured, not that the visit was instantaneous. If you want wall-clock, subtract first_event_ts from last_event_ts, both of which are returned.
utm_campaign and ref are informational. Neither influenced the ai_source, detection_method or stage on the same row. They are also customer-supplied free text echoed back unchanged, so escape them before rendering.
city and country_code are empty when they could not be resolved. Empty is a real, permanent value. Render it as a dash, not as the word “unknown”.
GET /v1/sessions/{id}/detail
Everything known about one session.
/detail, not /{id}. detection_trace is why the classifier decided what it did, so you can argue with a classification you disagree with: the rules that fired and the other evidence behind the score.
One field is removed before it reaches you: fingerprint_id, an identifier that is shared across sites by design, which is exactly why we do not hand it out. Nothing else in the trace is withheld.
A 404 means the session is no longer retained.
Session records live for at least 730 days and at most about 760.
The store’s TTL is toDateTime(partition_month) + INTERVAL 760 DAY, set by packages/db/clickhouse/migrations/071_raise_event_and_session_retention.sql. That migration is an ALTER, so the earlier files that created and rebuilt the table still read 120 and are no longer the live definition. partition_month is the start of the month the session began in, so a session that started on the 31st is measured from the 1st: 760 minus up to 30 days of slack leaves a guaranteed floor of 730. That floor is SESSION_GRAIN_RETENTION_DAYS in packages/db/src/clickhouse/queries.ts, and it is the number this API clamps to.
730 is the floor you can rely on, not the point of deletion. If you are restating our retention in your own privacy notice, quote the range.
A session can outlive its own events. When that happens you get a row back with an empty page_urls, which is the honest answer, rather than a 404 that would read as “no such session”.
exit_url, and why the two endpoints can disagree
exit_url is the last page measured in the session, the counterpart to entry_url. Both are page URLs, so the same query-parameter redaction applies to each.
The two endpoints compute it differently, and for older sessions they can return different answers for the same session:
- This endpoint derives it from the session’s own event records, so it is populated for the full raw-event retention window.
GET /v1/sessionsreads a value summarized when the session was recorded. Sessions that started before we began recording exit pages have no such value, so the field comes back empty there.
entry_url and exit_url rather than reporting an entry with no exit.
GET /v1/sessions/{id}/journey
Every AI touchpoint leading to this session’s conversion, with the credit each one gets under all four attribution models at once.
credit_fraction sums to 1.0 whenever there is at least one AI touch.
The session_id in every attribution entry is copied verbatim from touches, so you can join the two on it. All four arrays are built from the same touch list, so they are all populated or all empty. They never disagree about which sessions are involved, only about how much credit each one gets. The single-touch example above is the degenerate case where every model agrees; the models diverge once there are two or more touches.
When conversion is null the session has not converted yet. The model arrays are then a preview of how credit would be split, and every revenue_cents_attributed is 0.
Errors
Standard error shapes apply.Next
- Revenue API for the aggregate numbers these sessions add up to.
- Sources API for session counts per source.

