What the tracker sends (request payload)
The exact POST request the tracker sends to /api/event, every field in its JSON payload, and what is never sent: no cookies, identifiers or fingerprints.
Every hit is one POST to {origin}/api/event:
Content-Type: text/plain, so browsers don't send a CORS preflight,credentials: 'omit': no cookies are sent or accepted,keepalive: true, so hits survive page navigation.navigator.sendBeaconis the fallback whenfetchthrows.
The server responds with 202 Accepted with an empty body, whether or not the hit is later kept, so the response never tells a script anything about your settings.
Retries#
If the server responds with an error (any 4xx or 5xx status, including
429 when a rate limit is hit), the tracker sends the same hit again:
- up to 5 more times, after 1 second, 10 seconds, then 60 seconds for each of the last three,
- oldest hit first, one at a time,
- from a queue of at most 20 hits that lives in memory only. Nothing is written to the device, so hits still waiting are lost when the page is closed or reloaded.
A request that never gets a response (the visitor is offline, or a content blocker stops it) isn't retried.
A retried hit is recorded with the time it arrives, which can be a few minutes after it happened. In rare cases the server has already recorded a hit before the error reaches the browser, and the retry counts it a second time.
Body#
The three examples show every field that can appear in each kind of hit
the core script sends, in the order the script writes them. A real hit
carries far fewer: r, h, d, g, hp, ch, a, tz and xp are only
there in the cases the table lists.
A pageview:
{
"s": "pa_7Q2K9XH3AB",
"t": "pageview",
"u": "https://example.com/app?utm_source=newsletter#/pricing",
"l": "en-US",
"w": 1440,
"i": 0,
"r": "https://news.example.org/",
"h": 1,
"d": 1,
"g": 1,
"hp": 1,
"a": "webdriver",
"tz": "America/Denver",
"xp": "var_4M8T2KQ9ZD",
"p": { "plan": "pro" }
}
An engagement hit:
{
"s": "pa_7Q2K9XH3AB",
"t": "engagement",
"u": "https://example.com/app?utm_source=newsletter#/pricing",
"l": "en-US",
"w": 1440,
"i": 1,
"r": "https://news.example.org/",
"h": 1,
"d": 1,
"g": 1,
"hp": 1,
"a": "webdriver",
"tz": "America/Denver",
"e": 48210,
"sd": 80
}
A custom event:
{
"s": "pa_7Q2K9XH3AB",
"t": "event",
"u": "https://example.com/app?utm_source=newsletter#/pricing",
"l": "en-US",
"w": 1440,
"i": 1,
"r": "https://news.example.org/",
"h": 1,
"d": 1,
"g": 1,
"hp": 1,
"a": "webdriver",
"tz": "America/Denver",
"n": "signup",
"p": { "plan": "pro" }
}
| Key | Long name | Sent with | Meaning | What happens to it |
|---|---|---|---|---|
s |
site |
all | Site id | Used to find the site. Stored as the site the event belongs to |
t |
type |
all | pageview, event, engagement, vital or click |
Stored as the kind of event |
u |
url |
all | Page URL after exclusions, masks and query parameter stripping | Hostname and path are stored after your path masks and the PII scrubber, cut at 2,048 characters. Campaign parameters are stored, ad click ids become a yes/no flag, and the rest of the query is dropped |
l |
language |
all | navigator.language |
The first 16 characters are stored |
w |
screen_width |
all | screen.width in CSS pixels |
Stored only as a size bucket: xs (under 414), sm (under 768), md (under 1024), lg (under 1440) or xl. Also read in memory for the bot score. The exact width is dropped |
i |
interacted |
all | 1 once a real input was seen on the page, 0 before. See Interaction flag |
Stored as yes, no or unknown on the visit, never on the event |
r |
referrer |
when present | document.referrer, or a URL on your own site for SPA navigations |
Host and path are stored after the PII scrubber. The query string is dropped and your own domains are blanked |
h |
hash_mode |
when on | 1 if this URL's fragment is part of the path |
Used in memory to keep the fragment as part of the path. Not stored |
d |
dnt |
when on | 1 if Do Not Track is on |
If the site honors Do Not Track (off by default), the hit is dropped and nothing is stored. Otherwise ignored. The flag is never stored |
g |
gpc |
when on | 1 if Global Privacy Control is on |
If the site honors Global Privacy Control (on by default), the hit is dropped and nothing is stored. Otherwise ignored. The flag is never stored |
hp |
honeypot |
pages with data-hp |
1 on a honeypot page (data-hp="true"), 0 on a page marked data-hp="false" |
A hit with 1 never becomes a visit or an event and doesn't use your monthly events. It is counted as a bot and, with Advanced Bot Detection, kept as a honeypot hit. A hit with 0 is counted as usual |
ch |
challenge |
pages with data-hp="ch" |
1 on a "confirm you're human" page that a site shows a random few visitors, people and bots alike |
A hit with 1 never becomes a visit or an event and doesn't use your monthly events. It is not counted as a bot. With Advanced Bot Detection it is kept next to the honeypot hits, marked as a challenge |
a |
automation |
honeypot and challenge pages, when an automated browser is detected | webdriver, phantom, nightmare or cypress. On every other page the script sends nothing at all from an automated browser |
Kept with the honeypot hit. Any other value is read as none, and it is ignored on every other hit |
tz |
timezone |
all, when the browser reports one | The name of the browser's time zone, for example America/Denver. See Time zone |
Compared in memory with the country of the request for the bot score, then dropped. Never stored with a visit or an event and never used as a location. Kept only with a honeypot hit |
xp |
preview |
pageview, when set | The value of pa_preview in the address, from an experiment preview link |
Used in memory to show the version it names. The pageview is not counted for the experiment. Not stored |
p |
props |
pageview, event (every hit when data-props is set) |
Properties. See track(). A pageview with none sends {} |
Up to 30 are stored, strings cut at 500 characters, after the PII scrubber. revenue and currency are stored as their own fields |
e |
engaged_ms |
engagement | Visible time on the page in milliseconds | Stored. At most 4 hours is counted per hit |
sd |
scroll_depth |
engagement | Maximum scroll depth, 0 to 100. Left out of the one engagement hit sent at the first input when the engage module is off |
Stored in 10% bands |
n |
name |
event, vital | Event name, or the Web Vital (LCP, INP, CLS, FCP, TTFB) |
Stored, event names cut at 120 characters |
v |
value |
vital | The Web Vital value | Stored, for the share of hits your site's sampling rate keeps |
c |
selector |
click | Short CSS selector of the clicked element, or [data-privatus-click="name"] when the site named it |
Stored, cut at 512 characters |
The server also accepts rv/revenue and cu/currency at the top
level, but the tracker puts them in p.
A body larger than 32 KB, a body that isn't a JSON object and a hit with
an unknown t are ignored (still given a 202).
What each module sends#
The core script (pa.js) sends pageview, engagement and event hits.
The modules add these:
| Module | What it sends |
|---|---|
auto |
Custom events (t: "event") through track(), with the common fields above, n and p: Outbound Link (domain, url), File Download (file, host), Email Link (to_domain), Phone Link, Form Submit (form) and 404 (path) |
vitals |
One t: "vital" hit per metric when the page is hidden, with the common fields above, n (the metric) and v (its value) |
clicks |
One t: "click" hit per click, with the common fields above and c |
| Click map on your page | Only when a member opens Show on the page, never for visitors. Its own request, not /api/event: POST /api/overlay with t (the member's one-hour pass), u (the same page URL a pageview sends), period, device and h (1 when the page keeps its # part, as a hit says). No hit is sent while it is open. See click maps |
ask |
Its own two requests, not /api/event. GET /api/surveys asks which survey to show, with s (site id), u (the same page URL a pageview sends) and dnt=1 or gpc=1 when the browser sends that signal. POST /api/survey reports what happened, with s, u, d and g as above, id (the survey), k (shown, dismiss or answer), a (the answers) and done (1 when the survey was finished). See surveys |
Experiments#
A pageview is normally given a 202 and no body. When the page has
a running experiment and this visitor is
given another version, the response is 200 with a small JSON body:
{ "x": "/pricing-b", "p": 0 }
x is a path of the same site and p is 1 for a preview. The tracker
sends the visitor there and keeps the query string. That pageview is not
stored. The redirect adds pa_ref to the address with the page the
visitor really came from, so the visit keeps its source. The tracker
reads it once and removes it from the address bar. Nothing is stored in
the browser.
Interaction flag#
Some bots run a real browser, load a page and leave without touching it. To tell them from people, every hit says whether the tracker has seen a real input on the page:
iis0until the first pointer press or move, key press, touch or wheel turn, and1from then on.- At that first input the tracker sends one
engagementhit straight away, so the visit is marked even if the hit sent when the page closes is lost. - Scrolling alone doesn't count, and neither does input a script generates. Both are things a bot can do without a person.
It is a yes or no. Nothing about the input is sent: not what it was, not where the pointer was, not which key, not when. Visits are then reported as input seen, none or unknown, see Visits with no input seen.
Time zone#
Every hit carries the name of the browser's time zone (tz), as
Intl.DateTimeFormat().resolvedOptions().timeZone reports it.
A browser running on a server is rarely set to a time zone of the country its address is in. So the server compares the zone with the country of the request and feeds the result (it fits, it doesn't, it is UTC) into the bot score.
The zone is used in memory, for that comparison only. It is not stored with the visit or the event, not logged, and never used as a location: the country in reports comes only from the country header our network provider (Cloudflare) adds to the request, and a hit without that header has no country. The one place a time zone is kept is a honeypot hit.
What isn't sent#
No cookies, no stored identifiers, no user id, no screen height or color depth, no installed fonts or plugins, no canvas or audio fingerprints, no mouse movement, no form contents.
The server receives the IP address and User-Agent because every HTTP request carries them. They're used in memory and dropped. See How it works.